diff --git a/changelog/12376.feature.rst b/changelog/12376.feature.rst new file mode 100644 index 00000000000..49551b5b2a4 --- /dev/null +++ b/changelog/12376.feature.rst @@ -0,0 +1,7 @@ +Added :func:`pytest.register_fixture()` to register fixtures using an imperative interface. + +This is an advanced function intended for use by plugins. + +Normally, fixtures should be registered declaratively using the :func:`@pytest.fixture ` decorator. +Pytest looks for these fixture definitions during the collection phase and registers them automatically. +For some plugin usecases the declarative interface can be cumbersome or unviable, in which case this imperative interface can be used. diff --git a/changelog/14004.deprecation.rst b/changelog/14004.deprecation.rst index 594d943671a..d707b9cc309 100644 --- a/changelog/14004.deprecation.rst +++ b/changelog/14004.deprecation.rst @@ -2,5 +2,6 @@ Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to fixtu Use the ``node`` parameter instead for fixture scoping. This enables more robust node-based matching instead of string prefix matching. +If you've used ``nodeid=None``, pass ``node=session`` instead. This will be removed in pytest 10. diff --git a/changelog/14513.deprecation.rst b/changelog/14513.deprecation.rst new file mode 100644 index 00000000000..b5b17f06c06 --- /dev/null +++ b/changelog/14513.deprecation.rst @@ -0,0 +1,2 @@ +The private ``FixtureDef.has_location`` attribute is now deprecated and will be removed in pytest 10. +See :ref:`fixturedef-has-location-deprecated` for details. diff --git a/changelog/14513.improvement.rst b/changelog/14513.improvement.rst new file mode 100644 index 00000000000..5f952165638 --- /dev/null +++ b/changelog/14513.improvement.rst @@ -0,0 +1,5 @@ +The order in which fixture definitions overriding each other are resolved is now determined first by their *visibility* in the collection tree rather than by the order in which they happened to be registered. + +A fixture defined for a more specific node (e.g. a module or an item) now always takes precedence over one with the same name defined for a more general node (e.g. the session), even when the more general one was registered later. +Fixtures with non-comparable visibility keep the existing behavior of "last registered wins". +This change is supposed to only affect plugins which register multiple fixtures programmatically with the same name. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 6668e7393f3..280b0d94425 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -20,7 +20,7 @@ Below is a complete list of all pytest features which are considered deprecated. Passing ``baseid``/``nodeid`` strings to fixture registration APIs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 9.2 +.. deprecated:: 9.1 Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to ``FixtureManager._register_fixture`` and ``FixtureManager.parsefactories`` @@ -37,11 +37,27 @@ node-based matching instead of fragile string prefix matching. # Use instead fixture_manager.parsefactories(holder=plugin_obj, node=directory_node) - fixture_manager._register_fixture(name="fix", func=func, node=directory_node) + pytest.register_fixture(name="fix", func=func, node=directory_node) + +The equivalent of passing ``nodeid=None`` (global visibility) is ``node=session``. In pytest 10, the ``baseid`` and ``nodeid`` string parameters will be removed. +.. _fixturedef-has-location-deprecated: + +``FixtureDef.has_location`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1 + +The private ``FixtureDef.has_location`` attribute is deprecated and will be removed in pytest 10. + +It indicated whether a fixture was found from a node or a conftest in the collection tree (as opposed to a non-conftest plugin). +It was used to determine the override order of fixtures, pushing fixtures with "no location" to the front of the override chain (such that they are chosen last). +The override order is now determined by the visibility of the fixtures in the collection tree, making this distinction obsolete. + + .. _console-main: ``pytest.console_main()`` diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 70640f7efb1..95e75e60f32 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -123,6 +123,11 @@ "Pass node instead for fixture scoping." ) +FIXTUREDEF_HAS_LOCATION_DEPRECATED = PytestRemovedIn10Warning( + "FixtureDef.has_location is deprecated and will be removed in pytest 10. " + "See https://docs.pytest.org/en/stable/deprecations.html#fixturedef-has-location-deprecated" +) + PARSEFACTORIES_NODEID_DEPRECATED = PytestRemovedIn10Warning( "Passing nodeid string to parsefactories is deprecated. " "Use parsefactories(holder=obj, node=node) instead." diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 539648c8c44..9359c07e223 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -32,7 +32,6 @@ from typing import TypeVar import warnings -from .compat import deprecated import _pytest from _pytest import nodes from _pytest._code import getfslineno @@ -41,6 +40,7 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import assert_never +from _pytest.compat import deprecated from _pytest.compat import get_real_func from _pytest.compat import getfuncargnames from _pytest.compat import getimfunc @@ -60,6 +60,7 @@ from _pytest.deprecated import FIXTURE_BASEID_DEPRECATED from _pytest.deprecated import FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN from _pytest.deprecated import FIXTURE_NODEID_DEPRECATED +from _pytest.deprecated import FIXTUREDEF_HAS_LOCATION_DEPRECATED from _pytest.deprecated import PARSEFACTORIES_NODEID_DEPRECATED from _pytest.deprecated import YIELD_FIXTURE from _pytest.main import Session @@ -133,6 +134,31 @@ def get_scope_package( return node.session +def is_visibility_more_specific( + candidate: FixtureDef[Any], other: FixtureDef[Any] +) -> bool: + """Return whether the visibility of ``candidate`` is strictly more specific + than that of ``other``, i.e. ``candidate`` is defined on a strict descendant + in the collection tree of where ``other`` is defined.""" + if candidate.node is None or other.node is None: + # Fallback for fixtures registered with a string nodeid (deprecated). + # In this case compare baseids, which are nodeid prefixes. + # This branch can be removed once baseid deprecation is done (pytest 10). + if candidate.baseid == other.baseid: + return False + if other.baseid == "": + return True + # `candidate.baseid` must continue with a node separator for it to be a + # true descendant. + return candidate.baseid.startswith(other.baseid) and candidate.baseid[ + len(other.baseid) + ] in ("/", ":") + + return ( + candidate.node is not other.node and other.node in candidate.node.iter_parents() + ) + + def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: """Get the closest parent node (including self) which matches the given scope. @@ -1030,26 +1056,27 @@ class FixtureDef(Generic[FixtureValue]): def __init__( self, config: Config, - baseid: str | None, + baseid: str | None | NotSetType, argname: str, func: _FixtureFunc[FixtureValue], scope: Scope | ScopeName | Callable[[str, Config], ScopeName] | None, params: Sequence[object] | None, ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, *, - _ispytest: bool = False, + node: nodes.Node | NotSetType = NOTSET, # only used in a deprecationwarning msg, can be removed in pytest9 _autouse: bool = False, - node: nodes.Node | None = None, + _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) - # Emit deprecation warning if baseid string is used when node could be provided. - # baseid=None (global plugins) and baseid="" (synthetic fixtures) are fine. - if baseid and node is None: + # Emit deprecation warning if deprecated baseid string is used. + if node is NOTSET: warnings.warn(FIXTURE_BASEID_DEPRECATED, stacklevel=2) + if baseid is NOTSET: + baseid = None # The node where this fixture was defined, if available. # Used for node-based matching which is more robust than string matching. - self.node: Final = node + self.node: Final = node if node is not NOTSET else None # The "base" node ID for the fixture. # # This is a node ID prefix. A fixture is only available to a node (e.g. @@ -1064,11 +1091,15 @@ def __init__( # # For other plugins, the baseid is the empty string (always matches). # When node is available, baseid is derived from node.nodeid. - self.baseid: Final = node.nodeid if node is not None else (baseid or "") + # + # Deprecated: replaced by ``node``. + self.baseid: Final = node.nodeid if node is not NOTSET else (baseid or "") # Whether the fixture was found from a node or a conftest in the # collection tree. Will be false for fixtures defined in non-conftest # plugins. - self.has_location: Final = node is not None or baseid is not None + # + # Deprecated: kept only to back the deprecated ``has_location`` property. + self._has_location: Final = node is not NOTSET or baseid is not None # The fixture factory function. self.func: Final = func # The name by which the fixture may be requested. @@ -1103,6 +1134,11 @@ def scope(self) -> ScopeName: """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value + @property + def has_location(self) -> bool: + warnings.warn(FIXTUREDEF_HAS_LOCATION_DEPRECATED, stacklevel=2) + return self._has_location + def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) @@ -1217,11 +1253,12 @@ class RequestFixtureDef(FixtureDef[FixtureRequest]): def __init__(self, request: FixtureRequest) -> None: super().__init__( config=request.config, - baseid=None, + baseid=NOTSET, argname="request", func=lambda: request, scope=Scope.Function, params=None, + node=request.node, _ispytest=True, ) self.cached_result = (request, [0], None) @@ -1751,8 +1788,8 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> N # Store conftest for deferred parsing when its Directory is collected. self._pending_conftests[conftest_dir] = plugin else: - # Non-conftest plugins have global visibility (nodeid=None). - self.parsefactories(plugin, None) + # Non-conftest plugins have global visibility. + self.parsefactories(holder=plugin, node=self.session) @hookimpl(wrapper=True) def pytest_make_collect_report( @@ -1901,12 +1938,12 @@ def _register_fixture( *, name: str, func: _FixtureFunc[object], - nodeid: str | None = None, + nodeid: str | None | NotSetType = NOTSET, scope: Scope | ScopeName | Callable[[str, Config], ScopeName] = "function", params: Sequence[object] | None = None, ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, autouse: bool = False, - node: nodes.Node | None = None, + node: nodes.Node | NotSetType = NOTSET, ) -> None: """Register a fixture @@ -1930,13 +1967,12 @@ def _register_fixture( :param autouse: Whether this is an autouse fixture. """ - # Emit deprecation warning if nodeid string is used when node could be provided. - # nodeid=None (global plugins) is fine. - if nodeid and node is None: + # Emit deprecation warning if nodeid string. + if nodeid is not NOTSET or node is NOTSET: warnings.warn(FIXTURE_NODEID_DEPRECATED, stacklevel=2) fixture_def = FixtureDef( config=self.config, - baseid=nodeid if node is None else None, + baseid=nodeid, argname=name, func=func, scope=scope, @@ -1948,19 +1984,27 @@ def _register_fixture( ) faclist = self._arg2fixturedefs.setdefault(name, []) - if fixture_def.has_location: - faclist.append(fixture_def) + # Insert the fixturedef into the list while maintaining a partial order + # based on visibility: a fixturedef whose visibility is more specific + # sorts after a more general one, so that it takes precedence in the + # override chain (the last applicable fixturedef in the list is used + # first, see getfixturedefs). + # fixturedefs with the same visibility keep registration order, i.e. the + # last registered wins. + # The order between non-comparable fixturedefs doesn't matter since they + # cannot be visible together. + # The idea is that a fixture that is defined closer to the item should + # take precedence. + for i, existing in enumerate(faclist): + if is_visibility_more_specific(existing, fixture_def): + faclist.insert(i, fixture_def) + break else: - # fixturedefs with no location are at the front - # so this inserts the current fixturedef after the - # existing fixturedefs from external plugins but - # before the fixturedefs provided in conftests. - i = len([f for f in faclist if not f.has_location]) - faclist.insert(i, fixture_def) + faclist.append(fixture_def) if autouse: - if node is not None: + if node is not NOTSET: self._node_autousenames.setdefault(node, []).append(name) - elif nodeid: + elif nodeid is not NOTSET and nodeid is not None: # Legacy: plugin passed nodeid string without node reference. self._nodeid_autousenames.setdefault(nodeid, []).append(name) else: @@ -1975,6 +2019,9 @@ def parsefactories( raise NotImplementedError() @overload + @deprecated( + "parsefactories(obj, nodeid) is deprecated, use parsefactories(holder=obj, node=node) instead" + ) def parsefactories( self, node_or_obj: object, @@ -1985,8 +2032,8 @@ def parsefactories( @overload def parsefactories( self, - node_or_obj: None = ..., - nodeid: None = ..., + node_or_obj: NotSetType = ..., + nodeid: NotSetType = ..., *, holder: object, node: nodes.Node, @@ -1995,11 +2042,11 @@ def parsefactories( def parsefactories( self, - node_or_obj: nodes.Node | object | None = None, - nodeid: str | NotSetType | None = NOTSET, + node_or_obj: nodes.Node | object | NotSetType = NOTSET, + nodeid: str | None | NotSetType = NOTSET, *, - holder: object | None = None, - node: nodes.Node | None = None, + holder: object | NotSetType = NOTSET, + node: nodes.Node | NotSetType = NOTSET, ) -> None: """Collect fixtures from a collection node or object. @@ -2007,7 +2054,7 @@ def parsefactories( The preferred API uses keyword-only arguments: - ``holder``: The object to scan for fixtures. - - ``node``: The node determining fixture visibility scope. + - ``node``: The node determining fixture visibility. Legacy positional API (translated internally): - ``parsefactories(node)``: Uses node.obj as holder, node for scope. @@ -2015,24 +2062,22 @@ def parsefactories( """ # Translate legacy API to holder/node sources of truth # Either effective_node or effective_nodeid will be set, not both - effective_node: nodes.Node | None = None - effective_nodeid: str | None = None + effective_node: nodes.Node | NotSetType = NOTSET + effective_nodeid: str | None | NotSetType = NOTSET - if holder is not None: + if holder is not NOTSET: # New API: holder and node explicitly provided holderobj = holder effective_node = node - elif node_or_obj is None: + elif node_or_obj is NOTSET: raise TypeError("parsefactories() requires holder or node_or_obj") elif nodeid is not NOTSET: - # Legacy: parsefactories(obj, nodeid) - string-based scoping only - # Only warn if a non-None nodeid string is passed (None means global plugin) - if nodeid is not None: - warnings.warn(PARSEFACTORIES_NODEID_DEPRECATED, stacklevel=2) + # Legacy: parsefactories(obj, nodeid) - string-based scoping only. + warnings.warn(PARSEFACTORIES_NODEID_DEPRECATED, stacklevel=2) holderobj = node_or_obj effective_nodeid = nodeid else: - # Legacy: parsefactories(node) - node has .obj attribute + # parsefactories(node) - node has .obj attribute assert isinstance(node_or_obj, nodes.Node) holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined] effective_node = node_or_obj @@ -2277,3 +2322,62 @@ def _showfixtures_main(config: Config, session: Session) -> None: def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): tw.line(indent + line) + + +def register_fixture( + *, + name: str, + func: _FixtureFunc[object], + node: nodes.Node, + scope: Scope | ScopeName | Callable[[str, Config], ScopeName] = "function", + params: Sequence[object] | None = None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + autouse: bool = False, +) -> None: + """Register a fixture imperatively. + + This is an advanced function intended for use by plugins. + + Normally, fixtures should be registered declaratively using the + :func:`@pytest.fixture ` decorator. Pytest looks for these + fixture definitions during the collection phase and registers them + automatically. For some plugin usecases the declarative interface can be + cumbersome or unviable, in which case the imperative interface can be used. + + Fixture registration is expected to happen during the collection phase, and + this is the only sanctioned use. However, to allow for more creative uses, + this is not enforced. But do so at your own risk! + + .. versionadded: 9.1 + + :param name: + The fixture's name. + :param func: + The fixture's implementation function. + :param node: + The visibility of the fixture. + + Only items that are descendents of this node in the collection tree will + be able to request this fixture. You can think of this as the place + where you would put the `@pytest.fixture`. + + For global visibility, pass the :class:`session ` node, + which is the root of the collection tree. + :param scope: + The fixture's scope. + :param params: + The fixture's parametrization params. + :param ids: + The fixture's IDs. + :param autouse: + Whether this is an autouse fixture. + """ + node.session._fixturemanager._register_fixture( + name=name, + func=func, + node=node, + scope=scope, + params=params, + ids=ids, + autouse=autouse, + ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ad5a2c6a59b..45ff185184b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -591,7 +591,7 @@ def xunit_setup_module_fixture(request) -> Generator[None]: if teardown_module is not None: _call_with_optional_argument(teardown_module, module) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_module_fixture_{self.obj.__name__}", func=xunit_setup_module_fixture, @@ -627,7 +627,7 @@ def xunit_setup_function_fixture(request) -> Generator[None]: if teardown_function is not None: _call_with_optional_argument(teardown_function, function) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_function_fixture_{self.obj.__name__}", func=xunit_setup_function_fixture, @@ -807,7 +807,7 @@ def xunit_setup_class_fixture(request) -> Generator[None]: func = getimfunc(teardown_class) _call_with_optional_argument(func, cls) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", func=xunit_setup_class_fixture, @@ -841,7 +841,7 @@ def xunit_setup_method_fixture(request) -> Generator[None]: func = getattr(instance, teardown_name) _call_with_optional_argument(func, method) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", func=xunit_setup_method_fixture, @@ -1164,15 +1164,16 @@ class DirectParamFixtureDef(FixtureDef[FixtureValue]): usually behaves like any other FixtureDef. """ - def __init__(self, *, config: Config, argname: str, scope: Scope) -> None: + def __init__(self, *, node: nodes.Node, argname: str, scope: Scope) -> None: super().__init__( - config=config, - baseid="", + config=node.config, + baseid=NOTSET, argname=argname, func=get_direct_param_fixture_func, scope=scope, params=None, ids=None, + node=node, _ispytest=True, ) @@ -1395,7 +1396,7 @@ def parametrize( fixturedef = name2directparamfixturedef[argname] else: fixturedef = DirectParamFixtureDef( - config=self.config, + node=self.definition.session, argname=argname, scope=scope_, ) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 84435b3e09f..d5286af1470 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING from unittest import TestCase +from _pytest import fixtures import _pytest._code from _pytest._code import ExceptionInfo from _pytest.compat import assert_never @@ -170,7 +171,7 @@ def unittest_setup_class_fixture( cleanup() process_teardown_exceptions() - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_unittest_setUpClass_fixture_{cls.__qualname__}", func=unittest_setup_class_fixture, @@ -187,7 +188,7 @@ def unittest_skip_fixture(request: FixtureRequest) -> None: reason = getattr(cls, "__unittest_skip_why__", "") raise skip.Exception(reason, _use_item_location=True) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( name=f"_unittest_skip_fixture_{cls.__qualname__}", func=unittest_skip_fixture, node=self, @@ -216,7 +217,7 @@ def unittest_setup_method_fixture( if teardown is not None: teardown(self, request.function) - self.session._fixturemanager._register_fixture( + fixtures.register_fixture( # Use a unique name to speed up lookup. name=f"_unittest_setup_method_fixture_{cls.__qualname__}", func=unittest_setup_method_fixture, diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index a74136f77b5..7bce1d55ae3 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -26,6 +26,7 @@ from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import register_fixture from _pytest.fixtures import yield_fixture # type: ignore[deprecated] from _pytest.freeze_support import freeze_includes from _pytest.legacypath import TempdirFactory @@ -177,6 +178,7 @@ "param", "raises", "register_assert_rewrite", + "register_fixture", "set_trace", "skip", "version_tuple", diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5c4c535f979..7114344658f 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -174,7 +174,7 @@ def fix_b(): fm.parsefactories(mod_none, None) nodeid_warns = [x for x in w if "parsefactories" in str(x.message)] - assert len(nodeid_warns) == 1, f"Expected 1 warning, got: {w}" + assert len(nodeid_warns) == 2, f"Expected 2 warning, got: {w}" """ ) pytester.makepyfile( @@ -287,3 +287,26 @@ def test_scoped_invisible(request): ) result = pytester.runpytest("-W", "ignore::pytest.PytestRemovedIn10Warning") result.assert_outcomes(passed=2) + + def test_fixturedef_has_location_deprecated(self, pytester: Pytester) -> None: + """Accessing FixtureDef.has_location warns.""" + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def fix(): + return 1 + + def test_it(request): + fixturedef = request._fixturemanager.getfixturedefs( + "fix", request._pyfuncitem + )[0] + with pytest.warns( + pytest.PytestRemovedIn10Warning, match="has_location" + ): + assert fixturedef.has_location is True + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 9b85e1b388d..dc9474c6fd0 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1903,6 +1903,34 @@ def test_hello(self, item, fm): reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) + def test_register_fixture_ordered_by_visibility(self, pytester: Pytester) -> None: + """A fixturedef registered for a more specific node takes precedence + over one registered for a more general (ancestor) node, regardless of + the order in which they were registered (#14513).""" + pytester.makeconftest( + """ + import pytest + + @pytest.hookimpl(wrapper=True) + def pytest_collection(session): + result = yield + item = session.items[0] + pytest.register_fixture(name="fix", func=lambda: "session1", node=session) + pytest.register_fixture(name="fix", func=lambda fix: f"item1-{fix}", node=item) + pytest.register_fixture(name="fix", func=lambda fix: f"item2-{fix}", node=item) + pytest.register_fixture(name="fix", func=lambda: "session2", node=session) + return result + """ + ) + pytester.makepyfile( + """ + def test(fix): + assert fix == "item2-item1-session2" + """ + ) + reprec = pytester.inline_run() + reprec.assertoutcome(passed=1) + def test_parsefactories_relative_node_ids( self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 026589d65f5..96c4819e127 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -10,6 +10,7 @@ import textwrap from typing import Any from typing import cast +from typing import ClassVar import hypothesis from hypothesis import strategies @@ -44,7 +45,9 @@ class FixtureManagerMock: @dataclasses.dataclass class SessionMock: + config: Any _fixturemanager: FixtureManagerMock + nodeid: ClassVar = "" @dataclasses.dataclass class DefinitionMock(python.FunctionDefinition): @@ -55,7 +58,7 @@ class DefinitionMock(python.FunctionDefinition): fixtureinfo: Any = FuncFixtureInfoMock(names) definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid") definition._fixtureinfo = fixtureinfo - definition.session = SessionMock(FixtureManagerMock({})) + definition.session = SessionMock(config, FixtureManagerMock({})) return python.Metafunc(definition, fixtureinfo, config, _ispytest=True) def test_no_funcargs(self) -> None: