Source code for behave_kit.steps.data_driven

"""`@data_driven` — run a step once per row of a data file.

Loads test data via `behave_kit.data.load_data` and executes the decorated
step function once for each row, injecting the row's keys as keyword
arguments (with ``-`` replaced by ``_`` for valid Python identifiers).
"""

from __future__ import annotations

import functools
from collections.abc import Callable
from pathlib import Path
from typing import Any, Concatenate, ParamSpec, TypeVar, cast

from behave_kit._core.errors import BehaveKitError
from behave_kit._core.types import Context
from behave_kit.data.loader import load_examples_from

P = ParamSpec("P")
R = TypeVar("R")


def _sanitize_key(key: str) -> str:
    return key.replace("-", "_").replace(" ", "_")


[docs] def data_driven( path: str | Path, ) -> Callable[ [Callable[Concatenate[Context, P], R]], Callable[Concatenate[Context, P], R], ]: """Decorator: run the step once per row in the data file at ``path``. Each row (a ``dict``) is unpacked as keyword arguments into the step function. Column names with hyphens or spaces are converted to valid Python identifiers (``-`` and `` `` → ``_``). Usage:: @data_driven("tests/data/users.csv") @when("I login as {username}") def step(context, username=None, password=None): ... Raises: BehaveKitError: If the data file cannot be loaded or a row is not a dict. """ data_path = Path(path) def decorator( func: Callable[Concatenate[Context, P], R], ) -> Callable[Concatenate[Context, P], R]: """Wrap ``func`` to execute once per data row.""" @functools.wraps(func) def wrapper(context: Context, *args: P.args, **kwargs: P.kwargs) -> R: """Execute the step for each row in the data file.""" rows = load_examples_from(data_path) if not rows: raise BehaveKitError( f"No data rows found in '{data_path}'", suggestion="Ensure the file contains at least one data row", ) last_result: R | None = None for row in rows: if not isinstance(row, dict): raise BehaveKitError( f"Data row is {type(row).__name__}, expected dict", suggestion="Ensure the data file contains rows of key-value pairs", ) row_kwargs: dict[str, Any] = {_sanitize_key(k): v for k, v in row.items()} merged = {**row_kwargs, **kwargs} last_result = func(context, *args, **merged) return cast("R", last_result) return cast("Callable[Concatenate[Context, P], R]", wrapper) return decorator