behave-runner 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """behave-runner — CLI completa para Behave con subcomandos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,14 @@
1
+ """Entry point for python -m behave_runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_runner.cli.app import app
6
+
7
+
8
+ def main() -> None:
9
+ """Console script entry point."""
10
+ app()
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()
File without changes
@@ -0,0 +1,69 @@
1
+ """CLI app for behave-runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from behave_runner import __version__
8
+ from behave_runner.commands.config_cmd import config_app
9
+ from behave_runner.commands.doctor import doctor_command
10
+ from behave_runner.commands.format_cmd import format_command
11
+ from behave_runner.commands.generate import generate_app
12
+ from behave_runner.commands.impact import impact_command
13
+ from behave_runner.commands.init import init_command
14
+ from behave_runner.commands.lint import lint_command
15
+ from behave_runner.commands.list_cmd import list_command
16
+ from behave_runner.commands.open_cmd import open_command
17
+ from behave_runner.commands.record import record_command
18
+ from behave_runner.commands.report import report_app
19
+ from behave_runner.commands.run import run_command
20
+ from behave_runner.commands.select import select_command
21
+ from behave_runner.commands.steps import steps_app
22
+ from behave_runner.commands.trace import trace_app
23
+ from behave_runner.commands.watch import watch_command
24
+
25
+ app = typer.Typer(
26
+ name="behave-runner",
27
+ help="CLI completa para Behave con subcomandos.",
28
+ no_args_is_help=True,
29
+ rich_markup_mode="rich",
30
+ )
31
+
32
+
33
+ def version_callback(value: bool) -> None:
34
+ if value:
35
+ print(__version__)
36
+ raise typer.Exit
37
+
38
+
39
+ @app.callback()
40
+ def main(
41
+ version: bool = typer.Option(
42
+ None,
43
+ "--version",
44
+ callback=version_callback,
45
+ is_eager=True,
46
+ help="Show version and exit.",
47
+ ),
48
+ ) -> None:
49
+ """behave-runner — CLI completa para Behave con subcomandos."""
50
+ pass
51
+
52
+
53
+ app.command(name="run")(run_command)
54
+ app.command(name="select")(select_command)
55
+ app.command(name="list")(list_command)
56
+ app.command(name="format")(format_command)
57
+ app.command(name="lint")(lint_command)
58
+ app.command(name="doctor")(doctor_command)
59
+ app.command(name="init")(init_command)
60
+ app.add_typer(report_app, name="report")
61
+ app.add_typer(generate_app, name="generate")
62
+ app.add_typer(trace_app, name="trace")
63
+ app.command(name="watch")(watch_command)
64
+ app.command(name="impact")(impact_command)
65
+ app.add_typer(steps_app, name="steps")
66
+ app.command(name="impact")(impact_command)
67
+ app.command(name="record")(record_command)
68
+ app.command(name="open")(open_command)
69
+ app.add_typer(config_app, name="config")
File without changes
@@ -0,0 +1,173 @@
1
+ """Config command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from behave_runner.core.config import load_config
14
+
15
+ console = Console()
16
+
17
+ config_app = typer.Typer(
18
+ name="config",
19
+ help="Manage behave-runner configuration.",
20
+ no_args_is_help=True,
21
+ )
22
+
23
+
24
+ def _find_pyproject() -> Path:
25
+ """Find pyproject.toml in the current directory."""
26
+ pyproject = Path.cwd() / "pyproject.toml"
27
+ if not pyproject.exists():
28
+ console.print("[red]Error: pyproject.toml not found in current directory.[/red]")
29
+ raise typer.Exit(1)
30
+ return pyproject
31
+
32
+
33
+ def _read_toml(path: Path) -> dict[str, Any]:
34
+ """Read a TOML file and return its contents."""
35
+ with path.open("rb") as f:
36
+ return tomllib.load(f)
37
+
38
+
39
+ def _write_toml_section(path: Path, data: dict[str, Any]) -> None:
40
+ """Append or update [tool.behave-runner] section preserving the rest."""
41
+ content = path.read_text(encoding="utf-8")
42
+ section_header = "[tool.behave-runner]"
43
+
44
+ if section_header in content:
45
+ console.print("[yellow]Section [tool.behave-runner] already exists.[/yellow]")
46
+ return
47
+
48
+ lines = content.splitlines(keepends=True)
49
+ new_lines: list[str] = []
50
+ inserted = False
51
+
52
+ for line in lines:
53
+ new_lines.append(line)
54
+ if not inserted and line.strip().startswith("[tool."):
55
+ new_lines.append(f"\n{section_header}\n")
56
+ for key, value in data.items():
57
+ new_lines.append(f"{key} = {_format_value(value)}\n")
58
+ inserted = True
59
+
60
+ if not inserted:
61
+ if new_lines and not new_lines[-1].endswith("\n"):
62
+ new_lines[-1] += "\n"
63
+ new_lines.append(f"\n{section_header}\n")
64
+ for key, value in data.items():
65
+ new_lines.append(f"{key} = {_format_value(value)}\n")
66
+
67
+ path.write_text("".join(new_lines), encoding="utf-8")
68
+
69
+
70
+ def _set_config_value(path: Path, key: str, value: str) -> None:
71
+ """Set a key in the [tool.behave-runner] section."""
72
+ content = path.read_text(encoding="utf-8")
73
+ section_header = "[tool.behave-runner]"
74
+
75
+ if section_header not in content:
76
+ _write_toml_section(path, {key: _parse_value(value)})
77
+ return
78
+
79
+ lines = content.splitlines(keepends=True)
80
+ new_lines: list[str] = []
81
+ in_section = False
82
+ key_found = False
83
+
84
+ for line in lines:
85
+ stripped = line.strip()
86
+
87
+ if stripped == section_header:
88
+ in_section = True
89
+ new_lines.append(line)
90
+ continue
91
+
92
+ if in_section and stripped.startswith("[") and stripped != section_header:
93
+ if not key_found:
94
+ new_lines.append(f"{key} = {_format_value(_parse_value(value))}\n")
95
+ key_found = True
96
+ in_section = False
97
+
98
+ if (
99
+ in_section
100
+ and stripped.startswith(f"{key} ")
101
+ or (in_section and stripped.startswith(f"{key}="))
102
+ ):
103
+ new_lines.append(f"{key} = {_format_value(_parse_value(value))}\n")
104
+ key_found = True
105
+ continue
106
+
107
+ new_lines.append(line)
108
+
109
+ if in_section and not key_found:
110
+ new_lines.append(f"{key} = {_format_value(_parse_value(value))}\n")
111
+
112
+ path.write_text("".join(new_lines), encoding="utf-8")
113
+
114
+
115
+ def _format_value(value: Any) -> str:
116
+ """Format a Python value as a TOML literal."""
117
+ if isinstance(value, bool):
118
+ return "true" if value else "false"
119
+ if isinstance(value, int):
120
+ return str(value)
121
+ if isinstance(value, float):
122
+ return str(value)
123
+ return f'"{value}"'
124
+
125
+
126
+ def _parse_value(value: str) -> Any:
127
+ """Parse a string value into a Python type for TOML."""
128
+ if value.lower() in ("true", "false"):
129
+ return value.lower() == "true"
130
+ try:
131
+ return int(value)
132
+ except ValueError:
133
+ pass
134
+ try:
135
+ return float(value)
136
+ except ValueError:
137
+ pass
138
+ return value.strip("\"'")
139
+
140
+
141
+ @config_app.command(name="show")
142
+ def config_show() -> None:
143
+ """Show current behave-runner configuration."""
144
+ config = load_config()
145
+ if not config:
146
+ console.print("[yellow]No [tool.behave-runner] configuration found.[/yellow]")
147
+ return
148
+
149
+ table = Table(title="behave-runner configuration")
150
+ table.add_column("Key", style="cyan")
151
+ table.add_column("Value", style="green")
152
+ for key, value in sorted(config.items()):
153
+ table.add_row(key, str(value))
154
+ console.print(table)
155
+
156
+
157
+ @config_app.command(name="init")
158
+ def config_init() -> None:
159
+ """Create [tool.behave-runner] section in pyproject.toml."""
160
+ pyproject = _find_pyproject()
161
+ _write_toml_section(pyproject, {})
162
+ console.print("[green]Created [tool.behave-runner] section in pyproject.toml[/green]")
163
+
164
+
165
+ @config_app.command(name="set")
166
+ def config_set(
167
+ key: str = typer.Argument(..., help="Configuration key to set."),
168
+ value: str = typer.Argument(..., help="Value to assign."),
169
+ ) -> None:
170
+ """Set a configuration value in [tool.behave-runner]."""
171
+ pyproject = _find_pyproject()
172
+ _set_config_value(pyproject, key, value)
173
+ console.print(f"[green]Set {key} = {value}[/green]")
@@ -0,0 +1,30 @@
1
+ """Doctor command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from behave_runner.core.deps import check_optional
11
+
12
+ console = Console()
13
+
14
+
15
+ def doctor_command(
16
+ args: list[str] = typer.Argument(None, help="Arguments to pass to behave-doctor."),
17
+ ) -> None:
18
+ """Run behave-doctor to diagnose and fix common issues."""
19
+ if not check_optional("doctor", "behave_doctor", "doctor"):
20
+ raise typer.Exit(2)
21
+
22
+ cmd = ["behave-doctor", *(args or [])]
23
+ try:
24
+ result = subprocess.run(cmd, check=False) # noqa: S603
25
+ raise typer.Exit(result.returncode)
26
+ except FileNotFoundError:
27
+ console.print(
28
+ "[red]Error: behave-doctor not found. Install with: pip install behave-doctor[/red]"
29
+ )
30
+ raise typer.Exit(2) from None
@@ -0,0 +1,41 @@
1
+ """Format command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from behave_runner.core.deps import check_optional
11
+
12
+ console = Console()
13
+
14
+
15
+ def format_command(
16
+ args: list[str] = typer.Argument(None, help="Arguments to pass to behave-format."),
17
+ check: bool = typer.Option(False, "--check", help="Check only, don't modify files."),
18
+ diff: bool = typer.Option(False, "--diff", help="Show diff of changes."),
19
+ in_place: bool = typer.Option(False, "--in-place", help="Modify files in place."),
20
+ ) -> None:
21
+ """Run behave-format to format feature files."""
22
+ if not check_optional("format", "behave_format", "format"):
23
+ raise typer.Exit(2)
24
+
25
+ cmd = ["behave-format"]
26
+ if check:
27
+ cmd.append("--check")
28
+ if diff:
29
+ cmd.append("--diff")
30
+ if in_place:
31
+ cmd.append("--in-place")
32
+ cmd.extend(args or [])
33
+
34
+ try:
35
+ result = subprocess.run(cmd, check=False) # noqa: S603
36
+ raise typer.Exit(result.returncode)
37
+ except FileNotFoundError:
38
+ console.print(
39
+ "[red]Error: behave-format not found. Install with: pip install behave-format[/red]"
40
+ )
41
+ raise typer.Exit(2) from None
@@ -0,0 +1,53 @@
1
+ """Generate command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from behave_runner.core.deps import check_optional
11
+
12
+ console = Console()
13
+
14
+ generate_app = typer.Typer(
15
+ name="generate",
16
+ help="Generate step definitions and feature files using behave-gen.",
17
+ no_args_is_help=True,
18
+ )
19
+
20
+
21
+ def _run_behave_gen(cmd: list[str]) -> None:
22
+ """Delegate to behave-gen via subprocess."""
23
+ if not check_optional("gen", "behave_gen", "generate"):
24
+ raise typer.Exit(2)
25
+
26
+ try:
27
+ result = subprocess.run(cmd, check=False) # noqa: S603
28
+ raise typer.Exit(result.returncode)
29
+ except FileNotFoundError:
30
+ console.print(
31
+ "[red]Error: behave-gen not found. Install with: pip install behave-gen[/red]"
32
+ )
33
+ raise typer.Exit(2) from None
34
+
35
+
36
+ @generate_app.command(name="step")
37
+ def generate_step(
38
+ lib: str = typer.Option(..., "--lib", help="Step library name (e.g. http, auth)."),
39
+ ) -> None:
40
+ """Generate step definitions from a step library."""
41
+ _run_behave_gen(["behave-gen", "add", "steps", "--lib", lib])
42
+
43
+
44
+ @generate_app.command(name="feature")
45
+ def generate_feature(
46
+ name: str = typer.Argument(..., help="Feature name (without extension)."),
47
+ tags: str | None = typer.Option(None, "--tags", help="Comma or space separated tags."),
48
+ ) -> None:
49
+ """Generate a feature file skeleton."""
50
+ cmd = ["behave-gen", "add", "feature", name]
51
+ if tags:
52
+ cmd.extend(["--tags", tags])
53
+ _run_behave_gen(cmd)
@@ -0,0 +1,50 @@
1
+ """Impact command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from behave_runner.core.deps import check_optional
11
+ from behave_runner.core.orchestrator import RunConfig, run
12
+
13
+ console = Console()
14
+
15
+
16
+ def impact_command(
17
+ path: str = typer.Argument(".", help="Project root directory to analyze."),
18
+ fmt: str = typer.Option("text", "--format", help="Output format: text, json, sarif."),
19
+ run_affected: bool = typer.Option(
20
+ False, "--run", help="Run affected scenarios after detecting them."
21
+ ),
22
+ ) -> None:
23
+ """Detect scenarios affected by code changes using behave-doctor."""
24
+ if not check_optional("doctor", "behave_doctor", "impact"):
25
+ raise typer.Exit(2)
26
+
27
+ cmd = ["behave-doctor", "scan", "--format", fmt, path]
28
+
29
+ try:
30
+ result = subprocess.run( # noqa: S603
31
+ cmd, check=False, capture_output=run_affected, text=True
32
+ )
33
+ except FileNotFoundError:
34
+ console.print(
35
+ "[red]Error: behave-doctor not found. Install with: pip install behave-doctor[/red]"
36
+ )
37
+ raise typer.Exit(2) from None
38
+
39
+ if run_affected and result.stdout:
40
+ names = [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]
41
+ if names:
42
+ console.print(f"[cyan]Running {len(names)} affected scenarios...[/cyan]")
43
+ for name in names:
44
+ config = RunConfig(features=[path], name=[name])
45
+ run(config)
46
+ else:
47
+ console.print("[yellow]No affected scenarios to run.[/yellow]")
48
+ raise typer.Exit(result.returncode)
49
+
50
+ raise typer.Exit(result.returncode)
@@ -0,0 +1,37 @@
1
+ """Init command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from behave_runner.core.deps import check_optional
11
+
12
+ console = Console()
13
+
14
+
15
+ def init_command(
16
+ name: str | None = typer.Option(
17
+ None, "--name", help="Project name for the generated structure."
18
+ ),
19
+ args: list[str] = typer.Argument(None, help="Additional arguments for behave-gen."),
20
+ ) -> None:
21
+ """Initialize a new behave project structure using behave-gen."""
22
+ if not check_optional("gen", "behave_gen", "init"):
23
+ raise typer.Exit(2)
24
+
25
+ cmd = ["behave-gen", "init"]
26
+ if name:
27
+ cmd.extend(["--name", name])
28
+ cmd.extend(args or [])
29
+
30
+ try:
31
+ result = subprocess.run(cmd, check=False) # noqa: S603
32
+ raise typer.Exit(result.returncode)
33
+ except FileNotFoundError:
34
+ console.print(
35
+ "[red]Error: behave-gen not found. Install with: pip install behave-gen[/red]"
36
+ )
37
+ raise typer.Exit(2) from None
@@ -0,0 +1,30 @@
1
+ """Lint command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from behave_runner.core.deps import check_optional
11
+
12
+ console = Console()
13
+
14
+
15
+ def lint_command(
16
+ args: list[str] = typer.Argument(None, help="Arguments to pass to behave-lint."),
17
+ ) -> None:
18
+ """Run behave-lint static analysis on features."""
19
+ if not check_optional("lint", "behave_lint", "lint"):
20
+ raise typer.Exit(2)
21
+
22
+ cmd = [*args] if args else []
23
+ try:
24
+ result = subprocess.run(["behave-lint", *cmd], check=False) # noqa: S603
25
+ raise typer.Exit(result.returncode)
26
+ except FileNotFoundError:
27
+ console.print(
28
+ "[red]Error: behave-lint not found. Install with: pip install behave-lint[/red]"
29
+ )
30
+ raise typer.Exit(2) from None
@@ -0,0 +1,76 @@
1
+ """List command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import typer
10
+ from behave_model import load_feature
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ console = Console()
15
+
16
+
17
+ def _collect_scenarios(feature_paths: list[Path], tags: list[str]) -> list[dict[str, Any]]:
18
+ """Parse feature files and collect matching scenarios."""
19
+ scenarios: list[dict[str, Any]] = []
20
+ for fp in feature_paths:
21
+ if fp.is_dir():
22
+ feature_files = sorted(fp.rglob("*.feature"))
23
+ elif fp.is_file() and fp.suffix == ".feature":
24
+ feature_files = [fp]
25
+ else:
26
+ continue
27
+ for ff in feature_files:
28
+ feature = load_feature(str(ff))
29
+ for scenario in feature.scenarios:
30
+ if _matches_tags(scenario.tag_names, tags):
31
+ scenarios.append(
32
+ {
33
+ "feature": feature.name,
34
+ "scenario": scenario.name,
35
+ "location": str(scenario.location),
36
+ "tags": list(scenario.tag_names),
37
+ }
38
+ )
39
+ return scenarios
40
+
41
+
42
+ def _matches_tags(scenario_tags: list[str], filter_tags: list[str]) -> bool:
43
+ """Check if scenario matches all filter tags."""
44
+ if not filter_tags:
45
+ return True
46
+ return all(tag in scenario_tags for tag in filter_tags)
47
+
48
+
49
+ def list_command(
50
+ features: list[Path] = typer.Argument(
51
+ default=None,
52
+ help="Paths to feature files or directories.",
53
+ ),
54
+ tags: list[str] = typer.Option([], "--tags", "-t", help="Filter by tags."),
55
+ fmt: str = typer.Option("text", "--format", help="Output format: text, json."),
56
+ ) -> None:
57
+ """List scenarios without executing them."""
58
+ feature_paths = features if features else [Path("features")]
59
+ scenarios = _collect_scenarios(feature_paths, tags)
60
+
61
+ if fmt == "json":
62
+ print(json.dumps(scenarios, indent=2))
63
+ else:
64
+ table = Table(title="Scenarios")
65
+ table.add_column("Feature")
66
+ table.add_column("Scenario")
67
+ table.add_column("Location")
68
+ table.add_column("Tags")
69
+ for s in scenarios:
70
+ table.add_row(
71
+ s["feature"],
72
+ s["scenario"],
73
+ s["location"],
74
+ ", ".join(s["tags"]),
75
+ )
76
+ console.print(table)
@@ -0,0 +1,65 @@
1
+ """Open command for behave-runner CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from behave_runner.core.deps import check_optional
13
+ from behave_runner.core.output import find_latest_report
14
+ from behave_runner.utils import open_in_browser
15
+
16
+ console = Console()
17
+
18
+ TargetType = Literal["report", "trace"]
19
+
20
+
21
+ def open_command(
22
+ target: TargetType = typer.Argument(
23
+ "report",
24
+ help="What to open: 'report' (default) or 'trace'.",
25
+ ),
26
+ output: Path = typer.Option(
27
+ Path("reports"),
28
+ "--output",
29
+ help="Directory containing reports.",
30
+ ),
31
+ ) -> None:
32
+ """Open the latest report or trace viewer in the browser."""
33
+ if target == "trace":
34
+ _open_trace()
35
+ return
36
+
37
+ _open_report(output)
38
+
39
+
40
+ def _open_report(output: Path) -> None:
41
+ """Find and open the latest report in the browser."""
42
+ report_file = find_latest_report(output)
43
+ if report_file is None:
44
+ console.print("[yellow]No reports found.[/yellow]")
45
+ return
46
+ console.print(f"[green]Opening: {report_file}[/green]")
47
+ open_in_browser(str(report_file.resolve()))
48
+
49
+
50
+ def _open_trace() -> None:
51
+ """Open the trace viewer via behave-trace."""
52
+ if not check_optional("trace", "behave_trace", "trace"):
53
+ raise typer.Exit(2)
54
+
55
+ try:
56
+ result = subprocess.run(
57
+ ["behave-trace", "show"],
58
+ check=False, # noqa: S603
59
+ )
60
+ raise typer.Exit(result.returncode)
61
+ except FileNotFoundError:
62
+ console.print(
63
+ "[red]Error: behave-trace not found. Install with: pip install behave-trace[/red]"
64
+ )
65
+ raise typer.Exit(2) from None