Skip to content

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__

__post_init__() -> None

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() -> None

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() -> None

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

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}]"
        )