Skip to content

Commit 15e1a8a

Browse files
committed
Handle multiple parsers connected to a step function
1 parent 81a9264 commit 15e1a8a

File tree

4 files changed

+89
-19
lines changed

4 files changed

+89
-19
lines changed

pytest_bdd/scenario.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,19 @@ def find_argumented_step_fixture_name(
4343
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
4444
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
4545
for fixturedef in fixturedefs:
46-
parser = getattr(fixturedef.func, "parser", None)
47-
if parser is None:
48-
continue
49-
match = parser.is_matching(name)
50-
if not match:
51-
continue
52-
53-
parser_name = get_step_fixture_name(parser.name, type_)
54-
if request:
55-
try:
56-
request.getfixturevalue(parser_name)
57-
except FixtureLookupError:
46+
parsers = getattr(fixturedef.func, "_pytest_bdd_parsers", [])
47+
for parser in parsers:
48+
match = parser.is_matching(name)
49+
if not match:
5850
continue
59-
return parser_name
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
6059
return None
6160

6261

@@ -107,12 +106,16 @@ def _execute_step_function(request: FixtureRequest, scenario: Scenario, step: St
107106
converters = getattr(step_func, "converters", {})
108107
kwargs = {}
109108

110-
parser = getattr(step_func, "parser", None)
111-
if parser is not None:
109+
parsers = getattr(step_func, "_pytest_bdd_parsers", [])
110+
111+
for parser in parsers:
112+
if not parser.is_matching(step.name):
113+
continue
112114
for arg, value in parser.parse_arguments(step.name).items():
113115
if arg in converters:
114116
value = converters[arg](value)
115117
kwargs[arg] = value
118+
break
116119

117120
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in get_args(step_func)}
118121
kw["step_func_args"] = kwargs

pytest_bdd/steps.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def given_beautiful_article(article):
4343

4444
from .parsers import get_parser
4545
from .types import GIVEN, THEN, WHEN
46-
from .utils import get_caller_module_locals
46+
from .utils import get_caller_module_locals, setdefault
4747

4848
if typing.TYPE_CHECKING:
4949
from typing import Any, Callable
@@ -124,6 +124,8 @@ def decorator(func: Callable) -> Callable:
124124
parser_instance = get_parser(step_name)
125125
parsed_step_name = parser_instance.name
126126

127+
# TODO: Try to not attach to both step_func and lazy_step_func
128+
127129
step_func.__name__ = str(parsed_step_name)
128130

129131
def lazy_step_func() -> Callable:
@@ -135,7 +137,9 @@ def lazy_step_func() -> Callable:
135137
# Preserve the docstring
136138
lazy_step_func.__doc__ = func.__doc__
137139

138-
step_func.parser = lazy_step_func.parser = parser_instance
140+
setdefault(step_func, "_pytest_bdd_parsers", []).append(parser_instance)
141+
setdefault(lazy_step_func, "_pytest_bdd_parsers", []).append(parser_instance)
142+
139143
if converters:
140144
step_func.converters = lazy_step_func.converters = converters
141145

pytest_bdd/utils.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
import base64
55
import pickle
66
import re
7-
import typing
87
from inspect import getframeinfo, signature
98
from sys import _getframe
9+
from typing import TYPE_CHECKING, TypeVar
1010

11-
if typing.TYPE_CHECKING:
11+
if TYPE_CHECKING:
1212
from typing import Any, Callable
1313

1414
from _pytest.config import Config
1515
from _pytest.pytester import RunResult
1616

17+
T = TypeVar("T")
18+
1719
CONFIG_STACK: list[Config] = []
1820

1921

@@ -69,3 +71,12 @@ def collect_dumped_objects(result: RunResult) -> list:
6971
stdout = result.stdout.str() # pytest < 6.2, otherwise we could just do str(result.stdout)
7072
payloads = re.findall(rf"{_DUMP_START}(.*?){_DUMP_END}", stdout)
7173
return [pickle.loads(base64.b64decode(payload)) for payload in payloads]
74+
75+
76+
def setdefault(obj: object, name: str, default: T) -> T:
77+
"""Just like dict.setdefault, but for objects."""
78+
try:
79+
return getattr(obj, name)
80+
except AttributeError:
81+
setattr(obj, name, default)
82+
return default

tests/feature/test_steps.py

+52
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,58 @@ def check_results(results):
7272
result.assert_outcomes(passed=1, failed=0)
7373

7474

75+
def test_step_function_can_be_decorated_multiple_times(testdir):
76+
testdir.makefile(
77+
".feature",
78+
steps=textwrap.dedent(
79+
"""\
80+
Feature: Steps decoration
81+
82+
Scenario: Step function can be decorated multiple times
83+
Given there is a foo with value 42
84+
And there is a second foo with value 43
85+
When I do nothing
86+
And I do nothing again
87+
Then I make no mistakes
88+
And I make no mistakes again
89+
90+
"""
91+
),
92+
)
93+
testdir.makepyfile(
94+
textwrap.dedent(
95+
"""\
96+
from pytest_bdd import given, when, then, scenario, parsers
97+
98+
@scenario("steps.feature", "Step function can be decorated multiple times")
99+
def test_steps():
100+
pass
101+
102+
103+
@given(parsers.parse("there is a foo with value {value}"), target_fixture="foo")
104+
@given(parsers.parse("there is a second foo with value {value}"), target_fixture="second_foo")
105+
def foo(value):
106+
return value
107+
108+
109+
@when("I do nothing")
110+
@when("I do nothing again")
111+
def do_nothing():
112+
pass
113+
114+
115+
@then("I make no mistakes")
116+
@then("I make no mistakes again")
117+
def no_errors():
118+
assert True
119+
120+
"""
121+
)
122+
)
123+
result = testdir.runpytest()
124+
result.assert_outcomes(passed=1, failed=0)
125+
126+
75127
def test_all_steps_can_provide_fixtures(testdir):
76128
"""Test that given/when/then can all provide fixtures."""
77129
testdir.makefile(

0 commit comments

Comments
 (0)