Skip to content

Commit 7393b46

Browse files
authored
Merge pull request #530 from pytest-dev/fix-parsed-step-aliases
Handle multiple parsers connected to a step function
2 parents 81a9264 + 533b5cd commit 7393b46

File tree

6 files changed

+105
-30
lines changed

6 files changed

+105
-30
lines changed

Diff for: CHANGES.rst

+15-10
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
Changelog
22
=========
33

4+
6.0.1
5+
-----
6+
- Fix regression introduced in 6.0.0 where a step function decorated multiple using a parsers times would not be executed correctly. `#530 <https://github.com/pytest-dev/pytest-bdd/pull/530>`_ `#528 <https://github.com/pytest-dev/pytest-bdd/issues/528>`_
7+
8+
49
6.0.0
510
-----
611

712
This release introduces breaking changes in order to be more in line with the official gherkin specification.
813

9-
- Cleanup of the documentation and tests related to parametrization (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/469
10-
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/490
11-
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/492
12-
- Step arguments are no longer fixtures (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/493
13-
- Drop support of python 3.6, pytest 4 (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/495 https://github.com/pytest-dev/pytest-bdd/pull/504
14-
- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux) https://github.com/pytest-dev/pytest-bdd/pull/503
15-
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/499
16-
- Add type annotations (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505
17-
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods. (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505
18-
- Angular brackets in step definitions are only parsed in "Scenario Outline" (previously they were parsed also in normal "Scenario"s) (youtux) https://github.com/pytest-dev/pytest-bdd/pull/524.
14+
- Cleanup of the documentation and tests related to parametrization (elchupanebrej) `#469 <https://github.com/pytest-dev/pytest-bdd/pull/469>`_
15+
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) `#490 <https://github.com/pytest-dev/pytest-bdd/pull/490>`_
16+
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) `#492 <https://github.com/pytest-dev/pytest-bdd/pull/492>`_
17+
- Step arguments are no longer fixtures (olegpidsadnyi) `#493 <https://github.com/pytest-dev/pytest-bdd/pull/493>`_
18+
- Drop support of python 3.6, pytest 4 (elchupanebrej) `#495 <https://github.com/pytest-dev/pytest-bdd/pull/495>`_ `#504 <https://github.com/pytest-dev/pytest-bdd/issues/504>`_
19+
- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux) `#503 <https://github.com/pytest-dev/pytest-bdd/issues/503>`_
20+
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) `#499 <https://github.com/pytest-dev/pytest-bdd/pull/499>`_
21+
- Add type annotations (youtux) `#505 <https://github.com/pytest-dev/pytest-bdd/pull/505>`_
22+
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods. (youtux) `#505 <https://github.com/pytest-dev/pytest-bdd/pull/505>`_
23+
- Angular brackets in step definitions are only parsed in "Scenario Outline" (previously they were parsed also in normal "Scenario"s) (youtux) `#524 <https://github.com/pytest-dev/pytest-bdd/pull/524>`_.
1924

2025

2126

Diff for: pytest_bdd/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
from pytest_bdd.scenario import scenario, scenarios
55
from pytest_bdd.steps import given, then, when
66

7-
__version__ = "6.0.0"
7+
__version__ = "6.0.1"
88

99
__all__ = ["given", "when", "then", "scenario", "scenarios"]

Diff for: 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

Diff for: 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

Diff for: 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

Diff for: 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)