Skip to content
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

Adapt to getfixturedefs change in pytest 8.1 #666

Merged
merged 4 commits into from Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -3,6 +3,7 @@ Changelog

Unreleased
----------
- Address compatibility issue with pytest 8.1. `#666 <https://github.com/pytest-dev/pytest-bdd/pull/666>`_

7.0.1
-----
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Expand Up @@ -40,6 +40,7 @@ parse = "*"
parse-type = "*"
pytest = ">=6.2.0"
typing-extensions = "*"
packaging = "*"

[tool.poetry.group.dev.dependencies]
tox = ">=4.11.3"
Expand Down
21 changes: 21 additions & 0 deletions src/pytest_bdd/compat.py
@@ -0,0 +1,21 @@
from collections.abc import Sequence
from importlib.metadata import version
from typing import Optional

from _pytest.fixtures import FixtureDef, FixtureManager
from _pytest.nodes import Node
from packaging.version import Version
from packaging.version import parse as parse_version

pytest_version = parse_version(version("pytest"))


if pytest_version >= Version("8.1"):

def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Optional[Sequence[FixtureDef]]:
return fixturemanager.getfixturedefs(fixturename, node)

else:

def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Optional[Sequence[FixtureDef]]:
return fixturemanager.getfixturedefs(fixturename, node.nodeid)
5 changes: 3 additions & 2 deletions src/pytest_bdd/generation.py
Expand Up @@ -8,6 +8,7 @@
from _pytest._io import TerminalWriter
from mako.lookup import TemplateLookup

from .compat import getfixturedefs
from .feature import get_features
from .scenario import inject_fixturedefs_for_step, make_python_docstring, make_python_name, make_string_literal
from .steps import get_step_fixture_name
Expand Down Expand Up @@ -127,9 +128,9 @@ def _find_step_fixturedef(
fixturemanager: FixtureManager, item: Function, step: Step
) -> Sequence[FixtureDef[Any]] | None:
"""Find step fixturedef."""
with inject_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, nodeid=item.nodeid):
with inject_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, node=item):
bdd_name = get_step_fixture_name(step=step)
return fixturemanager.getfixturedefs(bdd_name, item.nodeid)
return getfixturedefs(fixturemanager, bdd_name, item)


def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]:
Expand Down
61 changes: 55 additions & 6 deletions src/pytest_bdd/scenario.py
Expand Up @@ -20,16 +20,17 @@

import pytest
from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func
from _pytest.nodes import iterparentnodeids
from typing_extensions import ParamSpec

from . import exceptions
from .compat import getfixturedefs
from .feature import get_feature, get_features
from .steps import StepFunctionContext, get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path

if TYPE_CHECKING:
from _pytest.mark.structures import ParameterSet
from _pytest.nodes import Node

from .parser import Feature, Scenario, ScenarioTemplate, Step

Expand All @@ -43,7 +44,7 @@
ALPHA_REGEX = re.compile(r"^\d+_*")


def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid: str) -> Iterable[FixtureDef[Any]]:
def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: Node) -> Iterable[FixtureDef[Any]]:
"""Find the fixture defs that can parse a step."""
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
fixture_def_by_name = list(fixturemanager._arg2fixturedefs.items())
Expand All @@ -60,14 +61,62 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid
if not match:
continue

if fixturedef not in (fixturemanager.getfixturedefs(fixturename, nodeid) or []):
fixturedefs = getfixturedefs(fixturemanager, fixturename, node)
if fixturedef not in (fixturedefs or []):
continue

yield fixturedef


# Function copied from pytest 8.0 (removed in later versions).
def iterparentnodeids(nodeid: str) -> Iterator[str]:
"""Return the parent node IDs of a given node ID, inclusive.

For the node ID

"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"

the result would be

""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"

Note that / components are only considered until the first ::.
"""
SEP = "/"
pos = 0
first_colons: Optional[int] = nodeid.find("::")
if first_colons == -1:
first_colons = None
# The root Session node - always present.
yield ""
# Eagerly consume SEP parts until first colons.
while True:
at = nodeid.find(SEP, pos, first_colons)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len(SEP)
# Eagerly consume :: parts.
while True:
at = nodeid.find("::", pos)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len("::")
# The node ID itself.
if nodeid:
yield nodeid


@contextlib.contextmanager
def inject_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid: str) -> Iterator[None]:
def inject_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: Node) -> Iterator[None]:
"""Inject fixture definitions that can parse a step.

We fist iterate over all the fixturedefs that can parse the step.
Expand All @@ -78,7 +127,7 @@ def inject_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node
"""
bdd_name = get_step_fixture_name(step=step)

fixturedefs = list(find_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, nodeid=nodeid))
fixturedefs = list(find_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, node=node))

# Sort the fixture definitions by their "path", so that the `bdd_name` fixture will
# respect the fixture scope
Expand Down Expand Up @@ -114,7 +163,7 @@ def get_step_function(request, step: Step) -> StepFunctionContext | None:
__tracebackhide__ = True
bdd_name = get_step_fixture_name(step=step)

with inject_fixturedefs_for_step(step=step, fixturemanager=request._fixturemanager, nodeid=request.node.nodeid):
with inject_fixturedefs_for_step(step=step, fixturemanager=request._fixturemanager, node=request.node):
try:
return cast(StepFunctionContext, request.getfixturevalue(bdd_name))
except pytest.FixtureLookupError:
Expand Down