Skip to content

Commit

Permalink
Merge pull request #530 from pytest-dev/fix-parsed-step-aliases
Browse files Browse the repository at this point in the history
Handle multiple parsers connected to a step function
  • Loading branch information
youtux authored Jul 7, 2022
2 parents 81a9264 + 533b5cd commit 7393b46
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 30 deletions.
25 changes: 15 additions & 10 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
Changelog
=========

6.0.1
-----
- 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>`_


6.0.0
-----

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

- Cleanup of the documentation and tests related to parametrization (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/469
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/490
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/492
- Step arguments are no longer fixtures (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/493
- 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
- 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
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/499
- Add type annotations (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505
- ``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
- 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.
- Cleanup of the documentation and tests related to parametrization (elchupanebrej) `#469 <https://github.com/pytest-dev/pytest-bdd/pull/469>`_
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) `#490 <https://github.com/pytest-dev/pytest-bdd/pull/490>`_
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) `#492 <https://github.com/pytest-dev/pytest-bdd/pull/492>`_
- Step arguments are no longer fixtures (olegpidsadnyi) `#493 <https://github.com/pytest-dev/pytest-bdd/pull/493>`_
- 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>`_
- 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>`_
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) `#499 <https://github.com/pytest-dev/pytest-bdd/pull/499>`_
- Add type annotations (youtux) `#505 <https://github.com/pytest-dev/pytest-bdd/pull/505>`_
- ``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>`_
- 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>`_.



Expand Down
2 changes: 1 addition & 1 deletion pytest_bdd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
from pytest_bdd.scenario import scenario, scenarios
from pytest_bdd.steps import given, then, when

__version__ = "6.0.0"
__version__ = "6.0.1"

__all__ = ["given", "when", "then", "scenario", "scenarios"]
33 changes: 18 additions & 15 deletions pytest_bdd/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,19 @@ def find_argumented_step_fixture_name(
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
for fixturedef in fixturedefs:
parser = getattr(fixturedef.func, "parser", None)
if parser is None:
continue
match = parser.is_matching(name)
if not match:
continue

parser_name = get_step_fixture_name(parser.name, type_)
if request:
try:
request.getfixturevalue(parser_name)
except FixtureLookupError:
parsers = getattr(fixturedef.func, "_pytest_bdd_parsers", [])
for parser in parsers:
match = parser.is_matching(name)
if not match:
continue
return parser_name

parser_name = get_step_fixture_name(parser.name, type_)
if request:
try:
request.getfixturevalue(parser_name)
except FixtureLookupError:
continue
return parser_name
return None


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

parser = getattr(step_func, "parser", None)
if parser is not None:
parsers = getattr(step_func, "_pytest_bdd_parsers", [])

for parser in parsers:
if not parser.is_matching(step.name):
continue
for arg, value in parser.parse_arguments(step.name).items():
if arg in converters:
value = converters[arg](value)
kwargs[arg] = value
break

kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in get_args(step_func)}
kw["step_func_args"] = kwargs
Expand Down
8 changes: 6 additions & 2 deletions pytest_bdd/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def given_beautiful_article(article):

from .parsers import get_parser
from .types import GIVEN, THEN, WHEN
from .utils import get_caller_module_locals
from .utils import get_caller_module_locals, setdefault

if typing.TYPE_CHECKING:
from typing import Any, Callable
Expand Down Expand Up @@ -124,6 +124,8 @@ def decorator(func: Callable) -> Callable:
parser_instance = get_parser(step_name)
parsed_step_name = parser_instance.name

# TODO: Try to not attach to both step_func and lazy_step_func

step_func.__name__ = str(parsed_step_name)

def lazy_step_func() -> Callable:
Expand All @@ -135,7 +137,9 @@ def lazy_step_func() -> Callable:
# Preserve the docstring
lazy_step_func.__doc__ = func.__doc__

step_func.parser = lazy_step_func.parser = parser_instance
setdefault(step_func, "_pytest_bdd_parsers", []).append(parser_instance)
setdefault(lazy_step_func, "_pytest_bdd_parsers", []).append(parser_instance)

if converters:
step_func.converters = lazy_step_func.converters = converters

Expand Down
15 changes: 13 additions & 2 deletions pytest_bdd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
import base64
import pickle
import re
import typing
from inspect import getframeinfo, signature
from sys import _getframe
from typing import TYPE_CHECKING, TypeVar

if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from typing import Any, Callable

from _pytest.config import Config
from _pytest.pytester import RunResult

T = TypeVar("T")

CONFIG_STACK: list[Config] = []


Expand Down Expand Up @@ -69,3 +71,12 @@ def collect_dumped_objects(result: RunResult) -> list:
stdout = result.stdout.str() # pytest < 6.2, otherwise we could just do str(result.stdout)
payloads = re.findall(rf"{_DUMP_START}(.*?){_DUMP_END}", stdout)
return [pickle.loads(base64.b64decode(payload)) for payload in payloads]


def setdefault(obj: object, name: str, default: T) -> T:
"""Just like dict.setdefault, but for objects."""
try:
return getattr(obj, name)
except AttributeError:
setattr(obj, name, default)
return default
52 changes: 52 additions & 0 deletions tests/feature/test_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,58 @@ def check_results(results):
result.assert_outcomes(passed=1, failed=0)


def test_step_function_can_be_decorated_multiple_times(testdir):
testdir.makefile(
".feature",
steps=textwrap.dedent(
"""\
Feature: Steps decoration
Scenario: Step function can be decorated multiple times
Given there is a foo with value 42
And there is a second foo with value 43
When I do nothing
And I do nothing again
Then I make no mistakes
And I make no mistakes again
"""
),
)
testdir.makepyfile(
textwrap.dedent(
"""\
from pytest_bdd import given, when, then, scenario, parsers
@scenario("steps.feature", "Step function can be decorated multiple times")
def test_steps():
pass
@given(parsers.parse("there is a foo with value {value}"), target_fixture="foo")
@given(parsers.parse("there is a second foo with value {value}"), target_fixture="second_foo")
def foo(value):
return value
@when("I do nothing")
@when("I do nothing again")
def do_nothing():
pass
@then("I make no mistakes")
@then("I make no mistakes again")
def no_errors():
assert True
"""
)
)
result = testdir.runpytest()
result.assert_outcomes(passed=1, failed=0)


def test_all_steps_can_provide_fixtures(testdir):
"""Test that given/when/then can all provide fixtures."""
testdir.makefile(
Expand Down

0 comments on commit 7393b46

Please sign in to comment.