Python API
behave-runner can also be used as a Python library. The public API is
kept small and lives mainly under behave_runner.core. This page is
auto-generated from the source docstrings with mkdocstrings.
Entry point
The behave_runner.__main__:main function is the same entry point used by
the behave-runner console script.
from behave_runner.__main__ import main
main()
For programmatic use, create a RunConfig and call run:
from behave_runner.core.orchestrator import RunConfig, run
config = RunConfig(features=["features"], tags=["@smoke"])
exit_code = run(config)
Core modules
Orchestrator
behave_runner.core.orchestrator
Orchestrator — builds behave commands from RunConfig.
RunConfig
dataclass
Configuration for a behave run.
Source code in behave_runner/core/orchestrator.py
| @dataclass(frozen=True)
class RunConfig:
"""Configuration for a behave run."""
features: list[str] = field(default_factory=lambda: ["features"])
tags: list[str] = field(default_factory=list)
dry_run: bool = False
stop_on_failure: bool = False
max_failures: int | None = None
timeout: int | None = None
fmt: str | None = None
outfile: str | None = None
name: list[str] = field(default_factory=list)
no_color: bool = False
verbose: bool = False
parallel: int | None = None
shard: str | None = None
parallel_scheme: str | None = None
parallel_balance: str | None = None
parallel_timing_file: str | None = None
retries: int | None = None
flaky_report: bool = False
priority_order: bool = False
fail_fast: bool = False
scenario_timeout: int | None = None
ui: bool = False
debug: bool = False
trace: bool = False
def __post_init__(self) -> None:
"""Validate field types and values."""
for field_name in ("features", "tags", "name"):
value = getattr(self, field_name)
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
raise ValueError(f"RunConfig.{field_name} must be a list of strings")
for field_name in ("parallel", "retries", "max_failures", "timeout", "scenario_timeout"):
value = getattr(self, field_name)
if value is None:
continue
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError(f"RunConfig.{field_name} must be an integer or None")
if value < 0:
raise ValueError(f"RunConfig.{field_name} must be a non-negative integer")
# parallel must be >= 1 if set (0 is meaningless — it silently falls back)
if self.parallel is not None and self.parallel < 1:
raise ValueError("RunConfig.parallel must be >= 1")
for field_name in (
"dry_run",
"stop_on_failure",
"flaky_report",
"priority_order",
"fail_fast",
"no_color",
"verbose",
"ui",
"debug",
"trace",
):
if not isinstance(getattr(self, field_name), bool):
raise ValueError(f"RunConfig.{field_name} must be a boolean")
for field_name in (
"fmt",
"outfile",
"shard",
"parallel_scheme",
"parallel_balance",
"parallel_timing_file",
):
value = getattr(self, field_name)
if value is not None and not isinstance(value, str):
raise ValueError(f"RunConfig.{field_name} must be a string or None")
if self.outfile == "":
raise ValueError("RunConfig.outfile cannot be an empty string")
if self.fmt == "":
raise ValueError("RunConfig.fmt cannot be an empty string")
if self.shard == "":
raise ValueError("RunConfig.shard cannot be an empty string")
if self.parallel_scheme == "":
raise ValueError("RunConfig.parallel_scheme cannot be an empty string")
if self.parallel_balance == "":
raise ValueError("RunConfig.parallel_balance cannot be an empty string")
if self.parallel_timing_file == "":
raise ValueError("RunConfig.parallel_timing_file cannot be an empty string")
|
__post_init__
Validate field types and values.
Source code in behave_runner/core/orchestrator.py
| def __post_init__(self) -> None:
"""Validate field types and values."""
for field_name in ("features", "tags", "name"):
value = getattr(self, field_name)
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
raise ValueError(f"RunConfig.{field_name} must be a list of strings")
for field_name in ("parallel", "retries", "max_failures", "timeout", "scenario_timeout"):
value = getattr(self, field_name)
if value is None:
continue
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError(f"RunConfig.{field_name} must be an integer or None")
if value < 0:
raise ValueError(f"RunConfig.{field_name} must be a non-negative integer")
# parallel must be >= 1 if set (0 is meaningless — it silently falls back)
if self.parallel is not None and self.parallel < 1:
raise ValueError("RunConfig.parallel must be >= 1")
for field_name in (
"dry_run",
"stop_on_failure",
"flaky_report",
"priority_order",
"fail_fast",
"no_color",
"verbose",
"ui",
"debug",
"trace",
):
if not isinstance(getattr(self, field_name), bool):
raise ValueError(f"RunConfig.{field_name} must be a boolean")
for field_name in (
"fmt",
"outfile",
"shard",
"parallel_scheme",
"parallel_balance",
"parallel_timing_file",
):
value = getattr(self, field_name)
if value is not None and not isinstance(value, str):
raise ValueError(f"RunConfig.{field_name} must be a string or None")
if self.outfile == "":
raise ValueError("RunConfig.outfile cannot be an empty string")
if self.fmt == "":
raise ValueError("RunConfig.fmt cannot be an empty string")
if self.shard == "":
raise ValueError("RunConfig.shard cannot be an empty string")
if self.parallel_scheme == "":
raise ValueError("RunConfig.parallel_scheme cannot be an empty string")
if self.parallel_balance == "":
raise ValueError("RunConfig.parallel_balance cannot be an empty string")
if self.parallel_timing_file == "":
raise ValueError("RunConfig.parallel_timing_file cannot be an empty string")
|
build_behave_command
build_behave_command(config: RunConfig) -> list[str]
Build the behave command as a list of strings for subprocess.
All optional features (parallel, trace, report formatters) are passed
directly to behave as flags. Behave handles them natively or via
installed formatter packages.
Uses "python -m behave" instead of "behave" to ensure the same Python
interpreter (and its installed packages) is used.
Source code in behave_runner/core/orchestrator.py
| def build_behave_command(config: RunConfig) -> list[str]:
"""Build the behave command as a list of strings for subprocess.
All optional features (parallel, trace, report formatters) are passed
directly to behave as flags. Behave handles them natively or via
installed formatter packages.
Uses "python -m behave" instead of "behave" to ensure the same Python
interpreter (and its installed packages) is used.
"""
cmd: list[str] = [sys.executable, "-m", "behave"]
cmd.extend(config.features)
for tag in config.tags:
cmd.extend(["--tags", tag])
if config.dry_run:
cmd.append("--dry-run")
if config.stop_on_failure:
cmd.append("--stop")
if config.max_failures is not None and config.max_failures > 0:
cmd.extend(["--stop"])
if config.timeout is not None:
cmd.extend(["--timeout", str(config.timeout)])
for name in config.name:
cmd.extend(["--name", name])
if config.no_color:
cmd.append("--no-color")
if config.verbose:
cmd.append("--verbose")
# Parallel: pass --parallel and related flags to behave.
# Behave 1.2.6 does not support --parallel natively; it requires
# behave-pool to register the flag. Only pass it when behave-pool
# is actually installed with real code (not just an empty namespace).
if config.parallel is not None and config.parallel > 1:
if _is_package_functional("behave_pool"):
cmd.extend(["--parallel", str(config.parallel)])
if config.parallel_scheme is not None:
cmd.extend(["--parallel-scheme", config.parallel_scheme])
if config.parallel_balance is not None:
cmd.extend(["--parallel-balance", config.parallel_balance])
if config.parallel_timing_file is not None:
cmd.extend(["--parallel-timing-file", config.parallel_timing_file])
else:
warnings.warn(
f"--parallel requires behave-pool to be installed; "
f"ignoring --parallel={config.parallel}.",
stacklevel=2,
)
# Shard: passed via BEHAVE_POOL_SHARD env var (set in _behave_env_vars),
# not as a CLI flag — behave does not support --shard natively.
# Format: either a report formatter or a behave built-in
# Only use the entry point name if the package is installed;
# otherwise fall back to behave's built-in formats.
if config.fmt is not None:
formatter = _resolve_formatter(config.fmt)
fmt_package = _REPORT_PACKAGES.get(config.fmt)
if formatter is not None and fmt_package is not None:
if is_installed(fmt_package):
cmd.extend(["--format", formatter])
else:
# Package not installed — pass through the raw name
# in case behave has a built-in with this name
cmd.extend(["--format", config.fmt])
else:
# Pass through as-is for behave built-in formats (plain, json, etc.)
cmd.extend(["--format", config.fmt])
# Output file for the format
if config.outfile is not None:
cmd.extend(["--outfile", config.outfile])
# Trace formatter: add as a second formatter alongside any report format
# Only when behave-trace is installed (graceful degradation otherwise)
if (config.trace or config.ui or config.debug) and is_installed("behave_trace"):
cmd.extend(["--format", "behave_trace:TraceFormatter"])
return cmd
|
run
run(config: RunConfig) -> int
Execute behave with the given config. Return exit code.
All optional features (parallel, trace, report formatters, retries,
priority) are passed to behave via command-line flags and environment
variables. Behave handles them natively or via installed packages.
Source code in behave_runner/core/orchestrator.py
| def run(config: RunConfig) -> int:
"""Execute behave with the given config. Return exit code.
All optional features (parallel, trace, report formatters, retries,
priority) are passed to behave via command-line flags and environment
variables. Behave handles them natively or via installed packages.
"""
behave_vars = _behave_env_vars(config)
saved = {k: os.environ.get(k) for k in behave_vars}
os.environ.update(behave_vars)
try:
cmd = build_behave_command(config)
env = _build_env(config)
return _run_behave_subprocess(cmd, env)
finally:
for key, old_value in saved.items():
if old_value is None:
os.environ.pop(key, None)
else:
os.environ[key] = old_value
|
validate_shard
validate_shard(shard: str) -> None
Validate a shard string in the format 'i/n' (e.g. '1/3').
Raises ValueError if the format is invalid or the shard index is out of range.
Source code in behave_runner/core/orchestrator.py
| def validate_shard(shard: str) -> None:
"""Validate a shard string in the format 'i/n' (e.g. '1/3').
Raises ValueError if the format is invalid or the shard index is out of range.
"""
match = _SHARD_RE.match(shard)
if not match:
raise ValueError(f"Invalid shard format: '{shard}'. Expected i/n (e.g. 1/3).")
i, n = int(match.group(1)), int(match.group(2))
if i < 1 or n < 1 or i > n:
raise ValueError(f"Invalid shard: {shard}. i must be 1..n.")
|
Configuration
behave_runner.core.config
Config file parsing for [tool.behave-runner] section.
load_config
load_config(project_path: Path | None = None) -> dict[str, Any]
Load [tool.behave-runner] config from pyproject.toml or behave.ini.
Source code in behave_runner/core/config.py
| def load_config(project_path: Path | None = None) -> dict[str, Any]:
"""Load [tool.behave-runner] config from pyproject.toml or behave.ini."""
root = project_path or Path.cwd()
pyproject = root / "pyproject.toml"
if pyproject.exists():
try:
with pyproject.open("rb") as f:
data = tomllib.load(f)
except tomllib.TOMLDecodeError as e:
raise ConfigError(f"Failed to parse {pyproject}: {e}") from e
except OSError as e:
raise ConfigError(f"Failed to read {pyproject}: {e}") from e
tool_section = data.get("tool")
if tool_section is None:
config = {}
elif not isinstance(tool_section, dict):
raise ConfigError(f"[tool] must be a table in {pyproject}")
else:
config = cast(dict[str, Any], tool_section.get("behave-runner", {}))
if not isinstance(config, dict):
raise ConfigError(f"[tool.behave-runner] must be a table in {pyproject}")
if config:
return config
behave_ini = root / "behave.ini"
if behave_ini.exists():
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read(behave_ini)
except (configparser.Error, UnicodeDecodeError) as e:
raise ConfigError(f"Failed to parse {behave_ini}: {e}") from e
if parser.has_section("behave-runner"):
flat = dict(parser.items("behave-runner"))
return _ini_flat_to_nested(flat)
return {}
|
load_profile
load_profile(name: str, project_path: Path | None = None) -> dict[str, Any]
Load a specific profile from config. Raises ConfigError if not found.
Source code in behave_runner/core/config.py
| def load_profile(name: str, project_path: Path | None = None) -> dict[str, Any]:
"""Load a specific profile from config. Raises ConfigError if not found."""
config = load_config(project_path)
profiles = config.get("profiles", {})
if not isinstance(profiles, dict):
raise ConfigError("Invalid 'profiles' configuration: must be a table.")
profile = cast(dict[str, Any] | None, profiles.get(name))
if profile is None:
raise ConfigError(f"Profile '{name}' not found in configuration.")
if not isinstance(profile, dict):
raise ConfigError(f"Profile '{name}' must be a table.")
return _normalize_profile(profile)
|
Dependency checking
behave_runner.core.deps
Optional dependency checking with graceful degradation.
check_optional
check_optional(feature: str, package: str, flag: str) -> bool
Check if an optional package is installed. Print warning if not.
Source code in behave_runner/core/deps.py
| def check_optional(feature: str, package: str, flag: str) -> bool:
"""Check if an optional package is installed. Print warning if not."""
if is_installed(package):
return True
console.print(
f"[yellow]Warning: {flag} requires {package}. "
f"Install with: pip install behave-runner[{feature}][/yellow]"
)
return False
|
is_installed
is_installed(package: str) -> bool
Silently check if a package is installed.
Source code in behave_runner/core/deps.py
| def is_installed(package: str) -> bool:
"""Silently check if a package is installed."""
try:
importlib.import_module(package)
return True
except ImportError:
return False
|
run_external
run_external(cmd: list[str], tool_name: str, install_hint: str) -> int
Run an external CLI tool via subprocess, handling common errors.
Parameters:
| Name |
Type |
Description |
Default |
cmd
|
list[str]
|
Command list to execute (passed to subprocess.run with shell=False).
|
required
|
tool_name
|
str
|
Human-readable tool name for error messages.
|
required
|
install_hint
|
str
|
Package name for the install instruction in error messages.
|
required
|
Returns:
| Type |
Description |
int
|
The tool's exit code, or 2 if the tool is not found or raises OSError.
|
Source code in behave_runner/core/deps.py
| def run_external(cmd: list[str], tool_name: str, install_hint: str) -> int:
"""Run an external CLI tool via subprocess, handling common errors.
Args:
cmd: Command list to execute (passed to subprocess.run with shell=False).
tool_name: Human-readable tool name for error messages.
install_hint: Package name for the install instruction in error messages.
Returns:
The tool's exit code, or 2 if the tool is not found or raises OSError.
"""
try:
result = subprocess.run(cmd, check=False) # noqa: S603 # nosec B603
return result.returncode
except FileNotFoundError:
console.print(
f"[red]Error: {tool_name} not found. Install with: pip install {install_hint}[/red]"
)
return 2
except OSError as e:
console.print(f"[red]Error running {tool_name}: {e}[/red]")
return 2
|
Output management
behave_runner.core.output
Output directory management for reports.
clean_output_dir
clean_output_dir(path: Path) -> None
Remove all contents of the output directory.
Symlinks are unlinked rather than followed to prevent deleting files
outside the output directory.
Source code in behave_runner/core/output.py
| def clean_output_dir(path: Path) -> None:
"""Remove all contents of the output directory.
Symlinks are unlinked rather than followed to prevent deleting files
outside the output directory.
"""
if not path.exists():
return
try:
items = list(path.iterdir())
except OSError:
return
for item in items:
try:
if item.is_symlink():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
except OSError:
continue
|
ensure_output_dir
ensure_output_dir(path: Path) -> Path
Create output directory if it doesn't exist. Return the path.
Raises FileExistsError if the path exists but is not a directory.
Source code in behave_runner/core/output.py
| def ensure_output_dir(path: Path) -> Path:
"""Create output directory if it doesn't exist. Return the path.
Raises FileExistsError if the path exists but is not a directory.
"""
if path.exists() and not path.is_dir():
raise FileExistsError(f"Path {path} exists but is not a directory")
path.mkdir(parents=True, exist_ok=True)
return path
|
find_latest_report
find_latest_report(output_dir: Path) -> Path | None
Find the most recently modified report file in output_dir.
Source code in behave_runner/core/output.py
| def find_latest_report(output_dir: Path) -> Path | None:
"""Find the most recently modified report file in output_dir."""
if not output_dir.exists():
return None
candidates: list[tuple[float, Path]] = []
for f in output_dir.iterdir():
try:
if f.is_file():
candidates.append((f.stat().st_mtime, f))
except OSError:
continue
if not candidates:
return None
candidates.sort(key=lambda pair: pair[0], reverse=True)
return candidates[0][1]
|
open_latest_report
open_latest_report(output_dir: Path) -> None
Find and open the latest report in the browser.
Source code in behave_runner/core/output.py
| def open_latest_report(output_dir: Path) -> None:
"""Find and open the latest report in the browser."""
report_file = find_latest_report(output_dir)
if report_file is None:
console.print("[yellow]No reports found.[/yellow]")
return
console.print(f"[green]Opening: {report_file}[/green]")
open_in_browser(str(report_file.resolve()))
|
File watcher
behave_runner.core.watcher
Polling-based file watcher with debounce.
FileWatcher
Polling-based file watcher with debounce.
Source code in behave_runner/core/watcher.py
| class FileWatcher:
"""Polling-based file watcher with debounce."""
def __init__(
self,
paths: list[Path],
on_change: Callable[[list[Path]], None],
debounce_ms: int = 500,
) -> None:
self._paths = paths
self._on_change = on_change
self._debounce = debounce_ms / 1000.0
self._mtimes: dict[Path, float] = {}
self._last_trigger = 0.0
self._running = False
def _scan(self) -> dict[Path, float]:
"""Scan all watched paths and return current mtimes."""
result: dict[Path, float] = {}
for path in self._paths:
try:
if path.is_file():
result[path] = path.stat().st_mtime
elif path.is_dir():
for f in path.rglob("*"):
try:
if f.is_file():
result[f] = f.stat().st_mtime
except OSError:
logger.debug("Could not stat %s during scan", f)
except OSError:
logger.debug("Could not scan %s", path)
return result
def _detect_changes(self) -> list[Path]:
"""Detect changed, added, or deleted files since last scan. Update internal state."""
current = self._scan()
changed: list[Path] = []
for path, mtime in current.items():
if path not in self._mtimes or self._mtimes[path] != mtime:
changed.append(path)
for path in self._mtimes:
if path not in current:
changed.append(path)
self._mtimes = current
return changed
def run(self) -> None:
"""Run the watcher loop. Blocks until stopped."""
self._running = True
self._mtimes = self._scan()
while self._running:
time.sleep(0.1)
now = time.time()
if now - self._last_trigger < self._debounce:
continue
changed = self._detect_changes()
if changed:
self._last_trigger = now
try:
self._on_change(changed)
except Exception:
logger.exception("Watcher callback failed for %s", changed)
def stop(self) -> None:
"""Stop the watcher loop."""
self._running = False
|
run
Run the watcher loop. Blocks until stopped.
Source code in behave_runner/core/watcher.py
| def run(self) -> None:
"""Run the watcher loop. Blocks until stopped."""
self._running = True
self._mtimes = self._scan()
while self._running:
time.sleep(0.1)
now = time.time()
if now - self._last_trigger < self._debounce:
continue
changed = self._detect_changes()
if changed:
self._last_trigger = now
try:
self._on_change(changed)
except Exception:
logger.exception("Watcher callback failed for %s", changed)
|
stop
Stop the watcher loop.
Source code in behave_runner/core/watcher.py
| def stop(self) -> None:
"""Stop the watcher loop."""
self._running = False
|
Feature parsing
behave_runner.core.features
Shared feature file parsing utilities.
ScenarioInfo
Bases: TypedDict
Information about a single scenario collected from a feature file.
Source code in behave_runner/core/features.py
| class ScenarioInfo(TypedDict):
"""Information about a single scenario collected from a feature file."""
feature: str
scenario: str
location: str
tags: list[str]
|
collect_scenarios
collect_scenarios(feature_paths: list[Path], tags: list[str] | None = None, pattern: str | None = None, feature_name: str | None = None) -> list[ScenarioInfo]
Parse feature files and collect scenarios matching all filters.
Parameters:
| Name |
Type |
Description |
Default |
feature_paths
|
list[Path]
|
Paths to feature files or directories.
|
required
|
tags
|
list[str] | None
|
Tag filters. Use ~@tag to exclude.
|
None
|
pattern
|
str | None
|
Regex pattern to match scenario names.
|
None
|
feature_name
|
str | None
|
Case-insensitive substring to filter feature names.
|
None
|
Source code in behave_runner/core/features.py
| def collect_scenarios(
feature_paths: list[Path],
tags: list[str] | None = None,
pattern: str | None = None,
feature_name: str | None = None,
) -> list[ScenarioInfo]:
"""Parse feature files and collect scenarios matching all filters.
Args:
feature_paths: Paths to feature files or directories.
tags: Tag filters. Use ~@tag to exclude.
pattern: Regex pattern to match scenario names.
feature_name: Case-insensitive substring to filter feature names.
"""
include_tags = [t.strip() for t in (tags or []) if not t.startswith("~") and t.strip()]
exclude_tags = [t[1:].strip() for t in (tags or []) if t.startswith("~") and t[1:].strip()]
regex = re.compile(pattern) if pattern else None
scenarios: list[ScenarioInfo] = []
for fp in feature_paths:
try:
if fp.is_dir():
feature_files = sorted(fp.rglob("*.feature"))
elif fp.is_file() and fp.suffix == ".feature":
feature_files = [fp]
else:
continue
except OSError as e:
logger.warning("Skipping %s: %s", fp, e)
continue
for ff in feature_files:
try:
feature = load_feature(str(ff))
except Exception as e:
logger.warning("Failed to load feature %s: %s", ff, e)
continue
if feature_name and (
not feature.name or feature_name.lower() not in feature.name.lower()
):
continue
for scenario in feature.scenarios:
scenario_tags = [t.strip() for t in scenario.tag_names]
if not matches_tags(scenario_tags, include_tags, exclude_tags):
continue
if regex and (not scenario.name or not regex.search(scenario.name)):
continue
scenarios.append(
ScenarioInfo(
feature=feature.name or "",
scenario=scenario.name or "",
location=str(scenario.location),
tags=scenario_tags,
)
)
return scenarios
|
matches_tags(scenario_tags: list[str], include_tags: list[str] | None = None, exclude_tags: list[str] | None = None) -> bool
Check if scenario matches include tags (AND) and excludes none.
If no include tags are specified, all scenarios pass the include check.
If no exclude tags are specified, no scenarios are excluded.
Source code in behave_runner/core/features.py
| def matches_tags(
scenario_tags: list[str],
include_tags: list[str] | None = None,
exclude_tags: list[str] | None = None,
) -> bool:
"""Check if scenario matches include tags (AND) and excludes none.
If no include tags are specified, all scenarios pass the include check.
If no exclude tags are specified, no scenarios are excluded.
"""
scenario_tags = [t.strip() for t in (scenario_tags or [])]
include = [t.strip() for t in (include_tags or []) if t.strip()]
exclude = [t.strip() for t in (exclude_tags or []) if t.strip()]
if include and not all(t in scenario_tags for t in include):
return False
if exclude:
return not any(t in scenario_tags for t in exclude)
return True
|
Utilities
behave_runner.utils
Utility functions for behave-runner.
find_project_root
find_project_root(start: Path | None = None) -> Path
Find project root by looking for pyproject.toml.
Source code in behave_runner/utils.py
| def find_project_root(start: Path | None = None) -> Path:
"""Find project root by looking for pyproject.toml."""
current = start or Path.cwd()
for parent in [current, *current.parents]:
if (parent / "pyproject.toml").exists():
return parent
return Path.cwd()
|
open_in_browser
open_in_browser(url: str) -> None
Open a URL in the default browser.
Set BEHAVE_RUNNER_NO_BROWSER=1 to skip opening the browser.
Source code in behave_runner/utils.py
| def open_in_browser(url: str) -> None:
"""Open a URL in the default browser.
Set `BEHAVE_RUNNER_NO_BROWSER=1` to skip opening the browser.
"""
if os.environ.get("BEHAVE_RUNNER_NO_BROWSER", "").lower() in ("1", "true", "yes"):
return
webbrowser.open(url)
|
Exceptions
behave_runner.exceptions
Custom exceptions for behave-runner.
BehaveRunnerError
Bases: Exception
Base exception for behave-runner.
Source code in behave_runner/exceptions.py
| class BehaveRunnerError(Exception):
"""Base exception for behave-runner."""
|
ConfigError
Bases: BehaveRunnerError
Raised when there is a configuration error.
Source code in behave_runner/exceptions.py
| class ConfigError(BehaveRunnerError):
"""Raised when there is a configuration error."""
|
DependencyMissingError
Bases: BehaveRunnerError
Raised when an optional dependency is required but not installed.
Source code in behave_runner/exceptions.py
| class DependencyMissingError(BehaveRunnerError):
"""Raised when an optional dependency is required but not installed."""
def __init__(self, feature: str, package: str) -> None:
self.feature = feature
self.package = package
super().__init__(
f"Feature '{feature}' requires '{package}'. "
f"Install with: pip install behave-runner[{feature}]"
)
|