"""Wiring layer: connect all behave-kit modules to a Behave context.
``setup()`` is idempotent and fault-tolerant — if one module fails to wire,
the others still work. ``teardown()`` only cleans up what was successfully
wired, in reverse order.
Usage in ``environment.py``::
from behave_kit import setup, teardown
def before_all(context):
setup(context, env="staging")
def after_scenario(context, scenario):
teardown(context)
"""
from __future__ import annotations
import logging
from behave_kit._core.errors import BehaveKitError
from behave_kit._core.logging import get_logger
from behave_kit._core.types import Context
logger = get_logger("hooks")
_WIRED_KEY = "_behave_kit_wired"
_FIXTURES_KEY = "_behave_kit_fixtures"
_SUGGESTIONS_KEY = "_behave_kit_suggestions"
def _validate_log_level(level: str) -> None:
try:
logging.getLogger().setLevel(level)
except (TypeError, ValueError) as exc:
raise BehaveKitError(
f"Invalid log_level '{level}'",
cause=exc,
suggestion="Use a logging level name such as DEBUG, INFO, WARNING, ERROR, or CRITICAL",
) from exc
def _wire_env_config(context: Context, env: str, config_file: str) -> None:
from behave_kit.env.config import load_env_config
load_env_config(context, env, config_file)
def _wire_soft_asserts(context: Context) -> None:
from behave_kit.assertions.soft import use_soft_asserts
use_soft_asserts(context)
def _wire_context_dump(context: Context) -> None:
from behave_kit.context.dump import dump_context_on_failure # noqa: F401
def _wire_suggestions(context: Context) -> None:
from behave_kit.steps.suggestions import setup_suggestions
hook = setup_suggestions(context)
setattr(context, _SUGGESTIONS_KEY, hook)
def _wire_fixtures(context: Context) -> None:
from behave_kit.fixtures import FixtureManager
manager = FixtureManager()
setattr(context, _FIXTURES_KEY, manager)
[docs]
def setup(
context: Context,
*,
env: str | None = None,
config_file: str = "behave.toml",
log_level: str = "INFO",
continue_after_failed: bool | None = None,
) -> None:
"""Wire all behave-kit modules into ``context``.
Idempotent: calling twice is a no-op. Each module is wired independently
in try/except — a failure in one does not prevent the others.
Args:
context: The Behave context object.
env: Optional environment name for profile-based configuration.
config_file: Path to the configuration file (default ``behave.toml``).
log_level: Logging level for the ``behave_kit`` logger.
continue_after_failed: When ``True``, scenarios continue executing
remaining steps after a failure. When ``False``, the default
Behave behaviour (stop on first failure) is restored. ``None``
leaves the current setting unchanged.
"""
_validate_log_level(log_level)
logging.getLogger("behave_kit").setLevel(log_level)
if hasattr(context, _WIRED_KEY):
return
wired: set[str] = set()
if continue_after_failed is not None:
try:
from behave_kit.continue_after_failed import continue_after_failed as _caf
_caf(continue_after_failed)
wired.add("continue_after_failed")
except Exception:
logger.warning("Failed to set continue_after_failed", exc_info=True)
if env is not None:
try:
_wire_env_config(context, env, config_file)
wired.add("env")
except Exception:
logger.warning("Failed to wire env config", exc_info=True)
try:
_wire_soft_asserts(context)
wired.add("soft")
except Exception:
logger.warning("Failed to wire soft asserts", exc_info=True)
try:
_wire_context_dump(context)
wired.add("dump")
except Exception:
logger.warning("Failed to wire context dump", exc_info=True)
try:
_wire_suggestions(context)
wired.add("suggestions")
except Exception:
logger.warning("Failed to wire suggestions", exc_info=True)
try:
_wire_fixtures(context)
wired.add("fixtures")
except Exception:
logger.warning("Failed to wire fixtures", exc_info=True)
setattr(context, _WIRED_KEY, wired)
def _teardown_fixtures(context: Context) -> None:
manager = getattr(context, _FIXTURES_KEY, None)
if manager is not None:
manager.teardown_scenario(context)
def _cleanup_scoped(context: Context) -> None:
from behave_kit.context.scoped import cleanup_scoped
cleanup_scoped(context)
def _report_soft_asserts(context: Context) -> None:
from behave_kit.assertions.soft import use_soft_asserts
collector = getattr(context, "_behave_kit_soft", None)
if collector is not None:
report = collector.report()
if report.has_failures:
logger.error("Soft assertion failures:\n%s", report)
try:
collector.raise_if_failed()
finally:
use_soft_asserts(context)
def _dump_if_failed(context: Context) -> None:
from behave_kit.context.dump import dump_context_on_failure
scenario = getattr(context, "scenario", None)
if scenario is not None:
dump_context_on_failure(context, scenario)
def _reset_continue_after_failed(context: Context) -> None:
from behave_kit.continue_after_failed import continue_after_failed as _caf
_caf(False)
[docs]
def teardown(context: Context) -> None:
"""Clean up wired modules in reverse order.
Only modules that were successfully wired during ``setup()`` are torn down.
Safe to call without a prior ``setup()`` (no-op). Also tears down any
class-based step instances created during the scenario, even if
``setup()`` was not called.
"""
wired: set[str] = getattr(context, _WIRED_KEY, set())
if "fixtures" in wired:
_teardown_fixtures(context)
_cleanup_scoped(context)
# Class-based step instances are torn down regardless of wiring,
# since they may be used with cherry-picked imports only.
_teardown_step_instances(context)
if "dump" in wired:
_dump_if_failed(context)
if "soft" in wired:
_report_soft_asserts(context)
if "continue_after_failed" in wired:
_reset_continue_after_failed(context)
def _teardown_step_instances(context: Context) -> None:
"""Tear down class-based step instances for ``context`` (no-op if none)."""
from behave_kit.steps.classes import teardown_steps
teardown_steps(context)