From cc86d99763b9ae0c6bbe08adc66b662bef378231 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 8 Jan 2024 21:52:40 +0200 Subject: [PATCH 1/4] Adapt to `getfixturedefs` change in pytest 8.1 The signature of this (private) function will change in the upcoming pytest 8.1 release: https://github.com/pytest-dev/pytest/pull/11785 Additionally, the `iterparentnodeids` function is removed, so copy/pasting it for now. I verified that all tests pass when run against pytest main. --- src/pytest_bdd/generation.py | 8 +++-- src/pytest_bdd/scenario.py | 62 ++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/pytest_bdd/generation.py b/src/pytest_bdd/generation.py index a4c96a56..81dbf97d 100644 --- a/src/pytest_bdd/generation.py +++ b/src/pytest_bdd/generation.py @@ -5,6 +5,7 @@ import os.path from typing import TYPE_CHECKING, cast +import pytest from _pytest._io import TerminalWriter from mako.lookup import TemplateLookup @@ -127,9 +128,12 @@ 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) + if hasattr(pytest, "version_tuple") and pytest.version_tuple >= (8, 1): + return fixturemanager.getfixturedefs(bdd_name, item) + else: + return fixturemanager.getfixturedefs(bdd_name, item.nodeid) def parse_feature_files(paths: list[str], **kwargs: Any) -> tuple[list[Feature], list[ScenarioTemplate], list[Step]]: diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index d64b3f61..f524d708 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -20,7 +20,6 @@ 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 @@ -43,7 +42,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) -> 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()) @@ -60,14 +59,65 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid if not match: continue - if fixturedef not in (fixturemanager.getfixturedefs(fixturename, nodeid) or []): + if hasattr(pytest, "version_tuple") and pytest.version_tuple >= (8, 1): + fixturedefs = fixturemanager.getfixturedefs(fixturename, node) + else: + fixturedefs = fixturemanager.getfixturedefs(fixturename, node.nodeid) + 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) -> Iterator[None]: """Inject fixture definitions that can parse a step. We fist iterate over all the fixturedefs that can parse the step. @@ -78,7 +128,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 @@ -114,7 +164,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: From 67d708f6f62318626d08616bf6553016eb220c8b Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:24:23 +0100 Subject: [PATCH 2/4] Extract `getfixturedefs` compatibility function --- poetry.lock | 2 +- pyproject.toml | 1 + src/pytest_bdd/compat.py | 21 +++++++++++++++++++++ src/pytest_bdd/generation.py | 7 ++----- src/pytest_bdd/scenario.py | 11 +++++------ 5 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 src/pytest_bdd/compat.py diff --git a/poetry.lock b/poetry.lock index a52c6a9f..6c83a679 100644 --- a/poetry.lock +++ b/poetry.lock @@ -552,4 +552,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "5b7935aa2b2d579d4fdb8361b414d37fff142a359332d859ed1513209ca3b1a6" +content-hash = "b40d47067f444deec4964404014795593f1b602f8a2f6376279bb5a27d5e18be" diff --git a/pyproject.toml b/pyproject.toml index 0ecc528f..3c163d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ parse = "*" parse-type = "*" pytest = ">=6.2.0" typing-extensions = "*" +packaging = "*" [tool.poetry.group.dev.dependencies] tox = ">=4.11.3" diff --git a/src/pytest_bdd/compat.py b/src/pytest_bdd/compat.py new file mode 100644 index 00000000..584c48fe --- /dev/null +++ b/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) diff --git a/src/pytest_bdd/generation.py b/src/pytest_bdd/generation.py index 81dbf97d..be4212f8 100644 --- a/src/pytest_bdd/generation.py +++ b/src/pytest_bdd/generation.py @@ -5,10 +5,10 @@ import os.path from typing import TYPE_CHECKING, cast -import pytest 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 @@ -130,10 +130,7 @@ def _find_step_fixturedef( """Find step fixturedef.""" with inject_fixturedefs_for_step(step=step, fixturemanager=fixturemanager, node=item): bdd_name = get_step_fixture_name(step=step) - if hasattr(pytest, "version_tuple") and pytest.version_tuple >= (8, 1): - return fixturemanager.getfixturedefs(bdd_name, item) - else: - 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]]: diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index f524d708..2fad5ee1 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -23,12 +23,14 @@ 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 @@ -42,7 +44,7 @@ ALPHA_REGEX = re.compile(r"^\d+_*") -def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node) -> 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()) @@ -59,10 +61,7 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node) if not match: continue - if hasattr(pytest, "version_tuple") and pytest.version_tuple >= (8, 1): - fixturedefs = fixturemanager.getfixturedefs(fixturename, node) - else: - fixturedefs = fixturemanager.getfixturedefs(fixturename, node.nodeid) + fixturedefs = getfixturedefs(fixturemanager, fixturename, node) if fixturedef not in (fixturedefs or []): continue @@ -117,7 +116,7 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]: @contextlib.contextmanager -def inject_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node) -> 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. From fa9659318b5025474d5cc4a760784ec577318b68 Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:31:16 +0100 Subject: [PATCH 3/4] Add changelog entry --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index a9986580..1c061de2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ Changelog Unreleased ---------- +- Address compatibility issue with pytest 8.1. `#666 `_ 7.0.1 ----- From 32a19ce09f7d2be160c733e337885b81e806b0cf Mon Sep 17 00:00:00 2001 From: Alessio Bogon <778703+youtux@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:41:48 +0100 Subject: [PATCH 4/4] Use annotations compatible with py3.8 --- src/pytest_bdd/compat.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pytest_bdd/compat.py b/src/pytest_bdd/compat.py index 584c48fe..ac732b56 100644 --- a/src/pytest_bdd/compat.py +++ b/src/pytest_bdd/compat.py @@ -1,6 +1,7 @@ +from __future__ import annotations + 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 @@ -12,10 +13,10 @@ if pytest_version >= Version("8.1"): - def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Optional[Sequence[FixtureDef]]: + def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None: return fixturemanager.getfixturedefs(fixturename, node) else: - def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Optional[Sequence[FixtureDef]]: + def getfixturedefs(fixturemanager: FixtureManager, fixturename: str, node: Node) -> Sequence[FixtureDef] | None: return fixturemanager.getfixturedefs(fixturename, node.nodeid)