"""Central registry for step metadata and behave integration."""from__future__importannotationsfromcollections.abcimportCallable,IteratorfromtypingimportAny,Protocolfromsteplib.core.decoratorsimportget_step_infosfromsteplib.core.exceptionsimportDuplicateStepError,StepContractErrorfromsteplib.core.i18nimportexpand_patternsfromsteplib.core.metadataimportStepInfo# User-facing decorator name used in error messages._STEP_DECORATOR_NAME="step"
[docs]classBehaveLikeRegistry(Protocol):"""Minimal protocol for behave's step registration API."""
[docs]defstep(self,pattern:str,)->Callable[[Callable[...,Any]],Callable[...,Any]]:"""Register a step pattern with behave. Args: pattern: The behave matching pattern. Returns: A decorator that attaches the function to the pattern. """...
[docs]classStepRegistry:"""Stores ``StepInfo`` entries and optionally registers them with behave. Args: auto_register_behave: When ``True``, each pattern is also registered with behave's global step registry via ``behave.step``. """def__init__(self,auto_register_behave:bool=True)->None:"""Initialize an empty registry."""self._steps:list[StepInfo]=[]self._patterns:dict[tuple[str,str|None],StepInfo]={}self._auto_register=auto_register_behave# --- public API ---
[docs]defadd(self,fn:Callable[...,Any])->None:"""Register all step metadata attached to *fn*. Extracts ``StepInfo`` entries from ``fn.__steplib_steps__``, expands i18n translations, checks for duplicates, and optionally registers each pattern with behave. Args: fn: A function decorated with ``@step``. Raises: StepContractError: If *fn* has no steplib step metadata. DuplicateStepError: If a pattern is already registered for the same backend. """infos=get_step_infos(fn)ifnotinfos:raiseStepContractError(f"Function '{fn.__qualname__}' is not a steplib step "f"(no @{_STEP_DECORATOR_NAME} metadata found).")forinfoininfos:self._add_info(info,fn)
[docs]deffilter(self,category:str|None=None,backend:str|None=None,tag:str|None=None,)->list[StepInfo]:"""Return steps matching the given filters (all optional, AND-combined). Args: category: Filter by category (e.g. ``"api"``). backend: Filter by backend (e.g. ``"httpx"``). tag: Filter by tag. Returns: A list of ``StepInfo`` entries matching all provided filters. """result:list[StepInfo]=[]forinfoinself._steps:ifcategoryisnotNoneandinfo.category!=category:continueifbackendisnotNoneandinfo.backend!=backend:continueiftagisnotNoneandtagnotininfo.tags:continueresult.append(info)returnresult
[docs]defget(self,pattern:str,backend:str|None=None)->StepInfo|None:"""Return the ``StepInfo`` for *pattern* (optionally filtered by backend). When *backend* is ``None``, returns the first match regardless of backend. Args: pattern: The step pattern to look up. backend: Optional backend to narrow the search. Returns: The matching ``StepInfo`` or ``None`` if not found. """ifbackendisnotNone:returnself._patterns.get((pattern,backend))# Search across all backends for the given pattern.for(pat,_be),infoinself._patterns.items():ifpat==pattern:returninforeturnNone
[docs]deffind(self,pattern:str,backend:str|None=None)->StepInfo|None:"""Alias for :meth:`get`. Args: pattern: The step pattern to look up. backend: Optional backend to narrow the search. Returns: The matching ``StepInfo`` or ``None`` if not found. """returnself.get(pattern,backend)
[docs]defavailable_backends(self,category:str|None=None)->set[str]:"""Return the set of backends present in the registry. Args: category: Optional category to narrow the search. Returns: A set of backend names. """backends:set[str]=set()forinfoinself._steps:ifinfo.backendisNone:continueifcategoryisnotNoneandinfo.category!=category:continuebackends.add(info.backend)returnbackends
[docs]defavailable_categories(self)->set[str]:"""Return the set of categories present in the registry. Returns: A set of category names. """return{info.categoryforinfoinself._steps}
[docs]defreplace_steps(self,kept:list[StepInfo])->None:"""Replace the registry's contents with *kept* steps only. Used by discovery filters to narrow the registry after loading. Rebuilds the internal pattern index from the kept steps. Args: kept: The ``StepInfo`` entries to keep. """self._steps=list(kept)self._patterns={}forinfoinkept:for_lang,patterninexpand_patterns(info):self._patterns[(pattern,info.backend)]=info
def__len__(self)->int:"""Return the number of registered steps."""returnlen(self._steps)def__iter__(self)->Iterator[StepInfo]:"""Iterate over registered ``StepInfo`` entries."""returniter(self._steps)# --- internals ---def_add_info(self,info:StepInfo,fn:Callable[...,Any])->None:for_lang,patterninexpand_patterns(info):key=(pattern,info.backend)ifkeyinself._patterns:raiseDuplicateStepError(pattern,info.backend)self._patterns[key]=infoifself._auto_register:self._register_with_behave(pattern,fn)self._steps.append(info)@staticmethoddef_register_with_behave(pattern:str,fn:Callable[...,Any])->None:"""Register a single pattern with behave's global step registry."""try:frombehaveimportstepasbehave_stepexceptImportError:# pragma: no coverreturnbehave_step(pattern)(fn)