Step Utilities

Custom parameter types, conditional steps, and “did you mean?” suggestions for undefined steps.

Custom parameter types

Register converters for custom step parameters:

behave_kit.steps.parameters.parameter_type(name: str, pattern: str = '') → Callable[[Callable[[str], Any]], Callable[[str], Any]][source]

Register func as the converter for parameter type name.

The type is also registered with Behave, so {param:name} placeholders in @given/@when/@then patterns (parse/cfparse matchers) are converted automatically. pattern is the regular expression used to match the parameter text; when omitted, Behave falls back to its default “anything” pattern.

behave_kit.steps.parameters.convert(name: str, match_string: str) → Any[source]

Apply the converter registered under name to match_string.

behave_kit.steps.parameters.register_builtin_types() → None[source]

Register every built-in converter. Safe to call more than once.

Registered types are also usable as {param:name} in Behave step patterns (e.g. {email:email}, {url:url}).

Built-in types

behave-kit ships converters for common types. Call register_builtin_types() once to register them all:

from behave_kit import register_builtin_types

def before_all(context):
    register_builtin_types()

Registered types are usable both via convert() and as {name:Type} placeholders in Behave step patterns (parse/cfparse matchers).

Available built-in types:

  • int — integer conversion

  • float — float conversion

  • date — ISO date (YYYY-MM-DD)

  • datetime — ISO datetime

  • decimal — Decimal for exact numeric comparisons

  • email — validated email address

  • url — validated HTTP/HTTPS URL

Custom parameter type example

from behave_kit import parameter_type, convert
from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str

@parameter_type("User", r'[\w.]+@[\w.]+')
def parse_user(text: str) -> User:
    name, domain = text.split("@")
    return User(name=name, email=text)

@when('I log in as "{user:User}"')
def step(context, user):
    # user is a User instance, not a raw string
    assert isinstance(user, User)
    context.current_user = user

Using convert() directly

from behave_kit import convert

email = convert("email", "alice@example.com")  # validated string
port = convert("int", "8080")                   # 8080 as int
url = convert("url", "https://example.com")     # validated URL

Conditional steps with @when_if

Run a step only when a condition function returns True:

behave_kit.steps.conditional.when_if(condition: Callable[[Context], object]) → Callable[[Callable[[Concatenate[Context, P]], R]], Callable[[Concatenate[Context, P]], R]][source]

Decorator: execute the step only if condition(context) is true.

When the condition is false, the step is skipped (via unittest.SkipTest) rather than executed or failed.

Examples

from behave_kit import when_if
from behave_kit.skip.conditions import is_env

@when("I run the staging-only step")
@when_if(lambda ctx: is_env(ctx, "staging"))
def step(context):
    ...

@when("I take a screenshot")
@when_if(lambda ctx: hasattr(ctx, "browser"))
def step(context):
    context.browser.save_screenshot("debug.png")

@when("I reset the test database")
@when_if(lambda ctx: not is_env(ctx, "production"))
def step(context):
    context.db.reset()

When the condition is False, the step is silently skipped (Behave marks it as skipped, not failed).

Data-driven steps with @data_driven

Run a step once per row of a data file (CSV, JSON, YAML, Excel). Column names are sanitized to valid Python identifiers and injected as keyword arguments. @data_driven must be placed inside (below) the Behave step decorator so Behave registers the wrapped function:

behave_kit.steps.data_driven.data_driven(path: str | Path) → Callable[[Callable[[Concatenate[Context, P]], R]], Callable[[Concatenate[Context, P]], R]][source]

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 are sanitized to valid Python identifiers (non-identifier characters become _).

Usage:

@when("I login as {username}")
@data_driven("tests/data/users.csv")
def step(context, username=None, password=None): ...

Note: @data_driven must be placed inside (below) the Behave step decorator so that the wrapped function is what Behave registers.

Raises:

BehaveKitError – If the data file cannot be loaded or a row is not a dict.

Examples

from behave_kit import data_driven

@when("I login with the test credentials")
@data_driven("tests/data/users.csv")
def step(context, username, password):
    login(username, password)
# Runs once per row in users.csv

Given tests/data/users.csv:

username,password
alice,secret
bob,pass123

The step is called twice with username="alice", password="secret" and username="bob", password="pass123".

Column name sanitization

Column names with hyphens or spaces are converted to valid Python identifiers:

user-name,pass word
alice,secret

The step receives user_name and pass_word as keyword arguments.

Using JSON or YAML

@when("I process each item")
@data_driven("tests/data/items.json")
def step(context, id, name):
    process(id, name)

The file must contain a list of dictionaries (or a single dict that will be wrapped in a list).

Step suggestions

When a step is undefined, behave-kit suggests the closest registered step patterns using difflib:

behave_kit.steps.suggestions.setup_suggestions(context: Context) → Callable[[Context, object], None][source]

Return an after_step(context, step) hook that logs suggestions.

Wire it in environment.py:

_suggest = behave_kit.steps.setup_suggestions(context)

def after_step(context, step):
    _suggest(context, step)
behave_kit.steps.suggestions.suggest_for_undefined(step: object) → list[str][source]

Log the closest registered step pattern(s) for an undefined step.

Returns the list of close matches found (possibly empty).

Wiring suggestions

Behave hooks cannot be injected — the returned after_step hook must be called explicitly. setup() attaches it to context.kit_suggestions:

Automatic (via `setup()`):

from behave_kit import setup

def before_all(context):
    setup(context, env="staging")

def after_step(context, step):
    context.kit_suggestions(context, step)

Manual:

from behave_kit import setup_suggestions

def before_all(context):
    context._suggest = setup_suggestions(context)

def after_step(context, step):
    context._suggest(context, step)

Example output

When a step is undefined, you’ll see a log message like:

INFO: Step 'I clikc the button' has not been defined. Did you mean: 'I click the button'?

This helps developers quickly identify typos in step definitions without searching through all registered steps.