Skip to content

Commit 27dfc13

Browse files
authored
Merge pull request #531 from pytest-dev/remove-clutter
Remove clutter from _step_decorator
2 parents 7393b46 + 092361a commit 27dfc13

File tree

4 files changed

+48
-90
lines changed

4 files changed

+48
-90
lines changed

pytest_bdd/parsers.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import abc
55
import re as base_re
6-
from typing import Any, Dict, cast
6+
from typing import Any, Dict, TypeVar, cast
77

88
import parse as base_parse
99
from parse_type import cfparse as base_cfparse
@@ -99,7 +99,10 @@ def is_matching(self, name: str) -> bool:
9999
return self.name == name
100100

101101

102-
def get_parser(step_name: Any) -> StepParser:
102+
TStepParser = TypeVar("TStepParser", bound=StepParser)
103+
104+
105+
def get_parser(step_name: str | TStepParser) -> TStepParser:
103106
"""Get parser by given name."""
104107

105108
if isinstance(step_name, StepParser):

pytest_bdd/scenario.py

+22-27
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,25 @@
3636
ALPHA_REGEX = re.compile(r"^\d+_*")
3737

3838

39-
def find_argumented_step_fixture_name(
40-
name: str, type_: str, fixturemanager: FixtureManager, request: FixtureRequest | None = None
41-
) -> str | None:
39+
def find_argumented_step_fixture_name(name: str, type_: str, fixturemanager: FixtureManager) -> str | None:
4240
"""Find argumented step fixture name."""
4341
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
4442
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
4543
for fixturedef in fixturedefs:
46-
parsers = getattr(fixturedef.func, "_pytest_bdd_parsers", [])
47-
for parser in parsers:
48-
match = parser.is_matching(name)
49-
if not match:
50-
continue
51-
52-
parser_name = get_step_fixture_name(parser.name, type_)
53-
if request:
54-
try:
55-
request.getfixturevalue(parser_name)
56-
except FixtureLookupError:
57-
continue
58-
return parser_name
44+
parser = getattr(fixturedef.func, "_pytest_bdd_parser", None)
45+
if parser is None:
46+
continue
47+
48+
match = parser.is_matching(name)
49+
if not match:
50+
continue
51+
52+
parser_name = get_step_fixture_name(parser.name, type_)
53+
return parser_name
5954
return None
6055

6156

62-
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> Any:
57+
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> Callable[..., Any]:
6358
"""Match the step defined by the regular expression pattern.
6459
6560
:param request: PyTest request object.
@@ -76,7 +71,7 @@ def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario)
7671
except FixtureLookupError as e:
7772
try:
7873
# Could not find a fixture with the same name, let's see if there is a parser involved
79-
argumented_name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager, request)
74+
argumented_name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager)
8075
if argumented_name:
8176
return request.getfixturevalue(argumented_name)
8277
raise e
@@ -87,7 +82,9 @@ def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario)
8782
) from e2
8883

8984

90-
def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable) -> None:
85+
def _execute_step_function(
86+
request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable[..., Any]
87+
) -> None:
9188
"""Execute step function.
9289
9390
:param request: PyTest request.
@@ -96,19 +93,16 @@ def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: St
9693
:param function step_func: Step function.
9794
:param example: Example table.
9895
"""
99-
kw = dict(request=request, feature=scenario.feature, scenario=scenario, step=step, step_func=step_func)
96+
kw = {"request": request, "feature": scenario.feature, "scenario": scenario, "step": step, "step_func": step_func}
10097

10198
request.config.hook.pytest_bdd_before_step(**kw)
102-
10399
kw["step_func_args"] = {}
104100
try:
105101
# Get the step argument values.
106-
converters = getattr(step_func, "converters", {})
102+
converters = step_func._pytest_bdd_converters
107103
kwargs = {}
108104

109-
parsers = getattr(step_func, "_pytest_bdd_parsers", [])
110-
111-
for parser in parsers:
105+
for parser in step_func._pytest_bdd_parsers:
112106
if not parser.is_matching(step.name):
113107
continue
114108
for arg, value in parser.parse_arguments(step.name).items():
@@ -117,11 +111,12 @@ def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: St
117111
kwargs[arg] = value
118112
break
119113

120-
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in get_args(step_func)}
114+
args = get_args(step_func)
115+
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
121116
kw["step_func_args"] = kwargs
122117

123118
request.config.hook.pytest_bdd_before_step_call(**kw)
124-
target_fixture = getattr(step_func, "target_fixture", None)
119+
target_fixture = step_func._pytest_bdd_target_fixture
125120

126121
# Execute the step as if it was a pytest fixture, so that we can allow "yield" statements in it
127122
return_value = call_fixture_func(fixturefunc=step_func, request=request, kwargs=kwargs)

pytest_bdd/steps.py

+21-30
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,16 @@ def given_beautiful_article(article):
3636
"""
3737
from __future__ import annotations
3838

39-
import typing
39+
from typing import Any, Callable, TypeVar
4040

4141
import pytest
4242
from _pytest.fixtures import FixtureDef, FixtureRequest
4343

44-
from .parsers import get_parser
44+
from .parsers import StepParser, get_parser
4545
from .types import GIVEN, THEN, WHEN
4646
from .utils import get_caller_module_locals, setdefault
4747

48-
if typing.TYPE_CHECKING:
49-
from typing import Any, Callable
48+
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
5049

5150

5251
def get_step_fixture_name(name: str, type_: str) -> str:
@@ -61,7 +60,7 @@ def get_step_fixture_name(name: str, type_: str) -> str:
6160

6261

6362
def given(
64-
name: Any,
63+
name: str | StepParser,
6564
converters: dict[str, Callable] | None = None,
6665
target_fixture: str | None = None,
6766
) -> Callable:
@@ -77,7 +76,9 @@ def given(
7776
return _step_decorator(GIVEN, name, converters=converters, target_fixture=target_fixture)
7877

7978

80-
def when(name: Any, converters: dict[str, Callable] | None = None, target_fixture: str | None = None) -> Callable:
79+
def when(
80+
name: str | StepParser, converters: dict[str, Callable] | None = None, target_fixture: str | None = None
81+
) -> Callable:
8182
"""When step decorator.
8283
8384
:param name: Step name or a parser object.
@@ -90,7 +91,9 @@ def when(name: Any, converters: dict[str, Callable] | None = None, target_fixtur
9091
return _step_decorator(WHEN, name, converters=converters, target_fixture=target_fixture)
9192

9293

93-
def then(name: Any, converters: dict[str, Callable] | None = None, target_fixture: str | None = None) -> Callable:
94+
def then(
95+
name: str | StepParser, converters: dict[str, Callable] | None = None, target_fixture: str | None = None
96+
) -> Callable:
9497
"""Then step decorator.
9598
9699
:param name: Step name or a parser object.
@@ -105,7 +108,7 @@ def then(name: Any, converters: dict[str, Callable] | None = None, target_fixtur
105108

106109
def _step_decorator(
107110
step_type: str,
108-
step_name: Any,
111+
step_name: str | StepParser,
109112
converters: dict[str, Callable] | None = None,
110113
target_fixture: str | None = None,
111114
) -> Callable:
@@ -118,38 +121,26 @@ def _step_decorator(
118121
119122
:return: Decorator function for the step.
120123
"""
124+
if converters is None:
125+
converters = {}
121126

122-
def decorator(func: Callable) -> Callable:
123-
step_func = func
127+
def decorator(func: TCallable) -> TCallable:
124128
parser_instance = get_parser(step_name)
125129
parsed_step_name = parser_instance.name
126130

127-
# TODO: Try to not attach to both step_func and lazy_step_func
128-
129-
step_func.__name__ = str(parsed_step_name)
130-
131-
def lazy_step_func() -> Callable:
132-
return step_func
133-
134-
step_func.step_type = step_type
135-
lazy_step_func.step_type = step_type
136-
137-
# Preserve the docstring
138-
lazy_step_func.__doc__ = func.__doc__
139-
140-
setdefault(step_func, "_pytest_bdd_parsers", []).append(parser_instance)
141-
setdefault(lazy_step_func, "_pytest_bdd_parsers", []).append(parser_instance)
131+
def lazy_step_func() -> TCallable:
132+
return func
142133

143-
if converters:
144-
step_func.converters = lazy_step_func.converters = converters
134+
lazy_step_func._pytest_bdd_parser = parser_instance
145135

146-
step_func.target_fixture = lazy_step_func.target_fixture = target_fixture
136+
setdefault(func, "_pytest_bdd_parsers", []).append(parser_instance)
137+
func._pytest_bdd_converters = converters
138+
func._pytest_bdd_target_fixture = target_fixture
147139

148-
lazy_step_func = pytest.fixture()(lazy_step_func)
149140
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
150141

151142
caller_locals = get_caller_module_locals()
152-
caller_locals[fixture_step_name] = lazy_step_func
143+
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(lazy_step_func)
153144
return func
154145

155146
return decorator

tests/steps/test_steps.py

-31
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import textwrap
44

5-
import pytest
6-
75

86
def test_when_then(testdir):
97
"""Test when and then steps are callable functions.
@@ -40,32 +38,3 @@ def test_when_then(request):
4038
)
4139
result = testdir.runpytest()
4240
result.assert_outcomes(passed=1)
43-
44-
45-
@pytest.mark.parametrize(
46-
("step", "keyword"),
47-
[("given", "Given"), ("when", "When"), ("then", "Then")],
48-
)
49-
def test_preserve_decorator(testdir, step, keyword):
50-
"""Check that we preserve original function attributes after decorating it."""
51-
testdir.makepyfile(
52-
textwrap.dedent(
53-
'''\
54-
from pytest_bdd import {step}
55-
from pytest_bdd.steps import get_step_fixture_name
56-
57-
@{step}("{keyword}")
58-
def func():
59-
"""Doc string."""
60-
61-
def test_decorator():
62-
assert globals()[get_step_fixture_name("{keyword}", {step}.__name__)].__doc__ == "Doc string."
63-
64-
65-
'''.format(
66-
step=step, keyword=keyword
67-
)
68-
)
69-
)
70-
result = testdir.runpytest()
71-
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)