Merge pull request #530 from pytest-dev/fix-parsed-step-aliases

Handle multiple parsers connected to a step function
This commit is contained in:
Alessio Bogon 2022-07-07 16:16:50 +02:00 committed by GitHub
commit 7393b46137
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 30 deletions

View File

@ -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>`_.

View File

@ -4,6 +4,6 @@ from __future__ import annotations
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"]

View File

@ -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
@ -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

View File

@ -43,7 +43,7 @@ from _pytest.fixtures import FixtureDef, FixtureRequest
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
@ -124,6 +124,8 @@ def _step_decorator(
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:
@ -135,7 +137,9 @@ def _step_decorator(
# 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

View File

@ -4,16 +4,18 @@ from __future__ import annotations
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] = []
@ -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

View File

@ -72,6 +72,58 @@ def test_steps(testdir):
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(