Source code for behave_kit.steps.parameters

"""`@parameter_type` — register custom step-parameter converters.

Ships built-in converters for common types (int, float, date, datetime,
decimal, email, url) via `register_builtin_types()`.
"""

from __future__ import annotations

import re
from collections.abc import Callable
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import Any

from behave_kit._core.errors import StepError
from behave_kit._core.registry import Registry

_EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
_URL_PATTERN = re.compile(r"^https?://\S+$")

_registry: Registry[Callable[[str], Any]] = Registry()
_patterns: dict[str, str] = {}


[docs] def parameter_type( name: str, pattern: str = "", ) -> Callable[[Callable[[str], Any]], Callable[[str], Any]]: """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. """ def decorator(func: Callable[[str], Any]) -> Callable[[str], Any]: """Register ``func`` as the converter for ``name``.""" _registry.register(name, func) _patterns[name] = pattern return func return decorator
[docs] def get_parameter_type_pattern(name: str) -> str | None: """Return the registered pattern for ``name``, or ``None`` if not registered.""" return _patterns.get(name)
[docs] def convert(name: str, match_string: str) -> Any: """Apply the converter registered under ``name`` to ``match_string``.""" if not isinstance(match_string, str): raise StepError( f"Cannot convert non-string value of type {type(match_string).__name__}", suggestion="Pass a string to convert()", ) try: converter = _registry.get(name) except Exception as exc: raise StepError( f"Parameter type '{name}' is not registered", cause=exc, suggestion=( f"Use a registered type or register one with @parameter_type('{name}', pattern)" ), ) from exc try: return converter(match_string) except StepError: raise except Exception as exc: raise StepError( f"Cannot convert '{match_string}' using '{name}'", cause=exc, suggestion="Check the input matches the expected format", ) from exc
def _to_int(value: str) -> int: try: return int(value) except ValueError as exc: raise StepError(f"Cannot convert '{value}' to int", cause=exc) from exc def _to_float(value: str) -> float: try: return float(value) except ValueError as exc: raise StepError(f"Cannot convert '{value}' to float", cause=exc) from exc def _to_date(value: str) -> date: try: return date.fromisoformat(value) except ValueError as exc: raise StepError( f"Cannot convert '{value}' to date (expected ISO format)", cause=exc ) from exc def _to_datetime(value: str) -> datetime: try: return datetime.fromisoformat(value) except ValueError as exc: raise StepError( f"Cannot convert '{value}' to datetime (expected ISO format)", cause=exc ) from exc def _to_decimal(value: str) -> Decimal: try: return Decimal(value) except InvalidOperation as exc: raise StepError(f"Cannot convert '{value}' to decimal", cause=exc) from exc def _to_email(value: str) -> str: if not _EMAIL_PATTERN.match(value): raise StepError(f"'{value}' is not a valid email address") return value def _to_url(value: str) -> str: if not _URL_PATTERN.match(value): raise StepError(f"'{value}' is not a valid URL") return value _BUILTIN_TYPES: dict[str, Callable[[str], Any]] = { "int": _to_int, "float": _to_float, "date": _to_date, "datetime": _to_datetime, "decimal": _to_decimal, "email": _to_email, "url": _to_url, }
[docs] def register_builtin_types() -> None: """Register every built-in converter. Safe to call more than once.""" registered = _registry.names() for name, func in _BUILTIN_TYPES.items(): if name not in registered: _registry.register(name, func)