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.

pattern documents the expected input shape; matching the pattern against step text is left to the caller (Cucumber Expressions, parse, or a manual regex) – this decorator only registers the conversion.

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.

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

Available built-in types:

  • int — integer conversion

  • float — float conversion

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

  • datetime — ISO datetime

  • decimalDecimal 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

@when_if(lambda ctx: ctx.config.env == "staging")
@when("I run the staging-only step")
def step(context):
    ...

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

@when_if(lambda ctx: ctx.config.env != "production")
@when("I reset the test database")
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 (hyphens and spaces become underscores) and injected as keyword arguments:

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 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.

Examples

from behave_kit import data_driven

@data_driven("tests/data/users.csv")
@when("I login with the test credentials")
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

@data_driven("tests/data/items.json")
@when("I process each item")
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

Automatic (via `setup()`):

from behave_kit import setup

def before_all(context):
    setup(context, env="staging")
# Suggestions are wired automatically.

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.