Skip to content

Commit 2f2c1b1

Browse files
Add possibility to register imported steps (#63)
* Add the possibility to register imported steps * Add test step definition hierarchy overlapping from pytest-dev#544
1 parent cf38255 commit 2f2c1b1

File tree

8 files changed

+486
-49
lines changed

8 files changed

+486
-49
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Planned
88
- Extra support of official parser
99
- https://github.com/pytest-dev/pytest-bdd/issues/502
1010

11+
1.2.2
12+
-----
13+
- Add possibility to register imported steps
14+
1115
1.2.0
1216
-----
1317
- Make liberal step definitions conform with

README.rst

+24
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,30 @@ default author.
156156
Given I'm the admin
157157
And there's an article
158158
159+
160+
Importing steps
161+
---------------
162+
Just import steps into test module or `conftest.py` is not enough to be found by pytest-bdd. To use steps from other
163+
module there are few possibilities:
164+
165+
- Register steps from module:
166+
167+
.. code-block:: python
168+
169+
import module_with_steps
170+
171+
step.from_module(module_with_steps)
172+
173+
- Import steps and register them from locals:
174+
175+
.. code-block:: python
176+
177+
from module_with_steps import given_a, when_b, then_c
178+
179+
step.from_locals()
180+
181+
- Build step_registry fixture and register imported steps there
182+
159183
Liberal step decorator
160184
----------------------
161185
Sometimes you want use same step for all types of steps without re-defining alias;

pytest_bdd/steps.py

+78-45
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def given_beautiful_article(article):
3838

3939
import warnings
4040
from contextlib import suppress
41-
from typing import Any, Callable, Iterable, Iterator, Sequence
41+
from typing import Any, Callable, Iterable, Iterator, Sequence, cast
4242
from warnings import warn
4343

4444
import pytest
@@ -49,7 +49,7 @@ def given_beautiful_article(article):
4949
from pytest_bdd.model import Feature, Scenario, Step
5050
from pytest_bdd.parsers import StepParser, get_parser
5151
from pytest_bdd.typing.pytest import Config, Parser, TypeAlias
52-
from pytest_bdd.utils import get_caller_module_locals
52+
from pytest_bdd.utils import deepattrgetter, get_caller_module_locals, setdefaultattr
5353
from pytest_bdd.warning_types import PytestBDDStepDefinitionWarning
5454

5555

@@ -302,16 +302,16 @@ def find_step_definition_matches(
302302
with suppress(AttributeError):
303303
yield from StepHandler.Matcher.find_step_definition_matches(registry.parent, matchers)
304304

305-
@attrs(auto_attribs=True)
305+
@attrs(auto_attribs=True, eq=False)
306306
class Definition:
307307
func: Callable
308308
type_: str | None
309309
parser: StepParser
310-
converters: dict[str, Any]
311-
params_fixtures_mapping: dict[str, str]
310+
converters: dict[str, Callable]
311+
params_fixtures_mapping: set[str] | dict[str, str] | Any
312312
param_defaults: dict
313313
target_fixtures: list[str]
314-
liberal: bool
314+
liberal: Any | None
315315

316316
def get_parameters(self, step: Step):
317317
parsed_arguments = self.parser.parse_arguments(step.name) or {}
@@ -322,43 +322,64 @@ def get_parameters(self, step: Step):
322322

323323
@attrs
324324
class Registry:
325-
registry: list[StepHandler.Definition] = attrib(default=Factory(list))
325+
registry: set[StepHandler.Definition] = attrib(default=Factory(set))
326326
parent: StepHandler.Registry = attrib(default=None, init=False)
327327

328328
@classmethod
329-
def register_step(
330-
cls,
331-
caller_locals: dict,
332-
func,
333-
type_,
334-
parserlike,
335-
converters,
336-
params_fixtures_mapping,
337-
param_defaults,
338-
target_fixtures,
339-
liberal,
340-
):
329+
def setdefault_step_registry_fixture(cls, caller_locals: dict):
341330
if "step_registry" not in caller_locals.keys():
342331
built_registry = cls()
343-
caller_locals["step_registry"] = built_registry.bind_pytest_bdd_step_registry_fixture()
344-
345-
registry: StepHandler.Registry = caller_locals["step_registry"].__registry__
346-
347-
parser = get_parser(parserlike)
348-
registry.registry.append(
349-
StepHandler.Definition( # type: ignore[call-arg]
350-
func=func,
351-
type_=type_,
352-
parser=parser,
353-
converters=converters,
354-
params_fixtures_mapping=params_fixtures_mapping,
355-
param_defaults=param_defaults,
356-
target_fixtures=target_fixtures,
357-
liberal=liberal,
358-
)
359-
)
332+
caller_locals["step_registry"] = built_registry.fixture
333+
return caller_locals["step_registry"]
334+
335+
@classmethod
336+
def register_step_definition(cls, step_definition, caller_locals: dict):
337+
fixture = cls.setdefault_step_registry_fixture(caller_locals=caller_locals)
338+
fixture.__registry__.registry.add(step_definition)
339+
340+
@classmethod
341+
def register_steps(cls, *step_funcs, caller_locals: dict):
342+
for step_func in step_funcs:
343+
for step_definition in step_func.__pytest_bdd_step_definitions__:
344+
cls.register_step_definition(step_definition, caller_locals=caller_locals)
345+
346+
@classmethod
347+
def register_steps_from_locals(cls, caller_locals=None, steps=None):
348+
if caller_locals is None:
349+
caller_locals = get_caller_module_locals(depth=2)
350+
351+
def registrable_steps():
352+
for name, obj in caller_locals.items():
353+
if hasattr(obj, "__pytest_bdd_step_definitions__") and (
354+
steps is None or any((name in steps, obj in steps))
355+
):
356+
yield obj
357+
358+
cls.register_steps(*registrable_steps(), caller_locals=caller_locals)
360359

361-
def bind_pytest_bdd_step_registry_fixture(self):
360+
@classmethod
361+
def register_steps_from_module(cls, module, caller_locals=None, steps=None):
362+
if caller_locals is None:
363+
caller_locals = get_caller_module_locals(depth=2)
364+
365+
def registrable_steps():
366+
# module items
367+
for name, obj in module.__dict__.items():
368+
if hasattr(obj, "__pytest_bdd_step_definitions__") and (
369+
steps is None or any((name in steps, obj in steps))
370+
):
371+
yield obj
372+
# module registry items
373+
for obj in deepattrgetter("__registry__.registry", default=None)(module.__dict__.get("step_registry"))[
374+
0
375+
]:
376+
if steps is None or obj.func in steps:
377+
yield obj.func
378+
379+
cls.register_steps(*set(registrable_steps()), caller_locals=caller_locals)
380+
381+
@property
382+
def fixture(self):
362383
@pytest.fixture
363384
def step_registry(step_registry):
364385
self.parent = step_registry
@@ -376,10 +397,10 @@ def decorator_builder(
376397
step_parserlike: Any,
377398
converters: dict[str, Callable] | None = None,
378399
target_fixture: str | None = None,
379-
target_fixtures: list[str] = None,
400+
target_fixtures: list[str] | None = None,
380401
params_fixtures_mapping: set[str] | dict[str, str] | Any = True,
381402
param_defaults: dict | None = None,
382-
liberal: bool | None = None,
403+
liberal: Any | None = None,
383404
) -> Callable:
384405
"""StepHandler decorator for the type and the name.
385406
@@ -414,17 +435,29 @@ def decorator(step_func: Callable) -> Callable:
414435
415436
:param function step_func: StepHandler definition function
416437
"""
417-
StepHandler.Registry.register_step(
418-
caller_locals=get_caller_module_locals(depth=2),
438+
439+
step_definiton = StepHandler.Definition( # type: ignore[call-arg]
419440
func=step_func,
420441
type_=step_type,
421-
parserlike=step_parserlike,
422-
converters=converters,
442+
parser=get_parser(step_parserlike),
443+
converters=cast(dict, converters),
423444
params_fixtures_mapping=params_fixtures_mapping,
424-
param_defaults=param_defaults,
425-
target_fixtures=target_fixtures,
445+
param_defaults=cast(dict, param_defaults),
446+
target_fixtures=cast(list, target_fixtures),
426447
liberal=liberal,
427448
)
449+
450+
setdefaultattr(step_func, "__pytest_bdd_step_definitions__", value_factory=set).add(step_definiton)
451+
452+
StepHandler.Registry.register_step_definition(
453+
step_definition=step_definiton,
454+
caller_locals=get_caller_module_locals(depth=2),
455+
)
456+
428457
return step_func
429458

430459
return decorator
460+
461+
462+
step.from_locals = StepHandler.Registry.register_steps_from_locals # type: ignore[attr-defined]
463+
step.from_module = StepHandler.Registry.register_steps_from_module # type: ignore[attr-defined]

pytest_bdd/typing/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import sys
55

66
if sys.version_info >= (3, 8):
7-
from typing import Protocol, runtime_checkable
7+
from typing import Literal, Protocol, runtime_checkable
88
else:
9-
from typing_extensions import Protocol, runtime_checkable
9+
from typing_extensions import Literal, Protocol, runtime_checkable
1010

11+
assert Literal
1112
assert Protocol
1213
assert runtime_checkable

pytest_bdd/utils.py

+16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
from collections import defaultdict
88
from contextlib import nullcontext, suppress
9+
from enum import Enum
910
from functools import partial
1011
from inspect import getframeinfo, signature
1112
from itertools import tee
@@ -17,6 +18,7 @@
1718
from marshmallow import post_load
1819

1920
from pytest_bdd.const import ALPHA_REGEX, PYTHON_REPLACE_REGEX
21+
from pytest_bdd.typing import Literal
2022
from pytest_bdd.typing.pytest import FixtureDef
2123

2224
if TYPE_CHECKING: # pragma: no cover
@@ -239,6 +241,20 @@ def _():
239241
return fn
240242

241243

244+
class EMPTY(Enum):
245+
EMPTY = 1
246+
247+
248+
def setdefaultattr(obj, key, value: Literal[EMPTY.EMPTY] | Any = EMPTY, value_factory: None | Callable = None):
249+
if value is not EMPTY and value_factory is not None:
250+
raise ValueError("Both 'value' and 'value_factory' were specified")
251+
if not hasattr(obj, key):
252+
if value_factory is not None:
253+
value = value_factory()
254+
setattr(obj, key, value)
255+
return getattr(obj, key, value)
256+
257+
242258
def make_python_name(string: str) -> str:
243259
"""Make python attribute name out of a given string."""
244260
string = re.sub(PYTHON_REPLACE_REGEX, "", string.replace(" ", "_"))

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ author = Oleg Pidsadnyi, Anatoly Bubenkov and others
77
license = MIT license
88
author_email = [email protected]
99
url = https://github.com/elchupanebrej/pytest-bdd-ng
10-
version = 1.2.1
10+
version = 1.2.2
1111
classifiers =
1212
Development Status :: 4 - Beta
1313
Intended Audience :: Developers

0 commit comments

Comments
 (0)