Skip to content

Handle multiple parsers connected to a step function #530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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