From 80b4f8be1cb3af8a218234f49b2593e26020096c Mon Sep 17 00:00:00 2001 From: Sadra Barikbin Date: Mon, 31 Jul 2023 02:03:58 +0330 Subject: [PATCH] Fix a bug --- src/_pytest/fixtures.py | 16 ++++++++++------ src/_pytest/python.py | 32 +++++++++++++++++--------------- testing/python/fixtures.py | 6 ++++-- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9b45f1478..c9240a474 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1414,11 +1414,10 @@ class FixtureManager: tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames ) - arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} - names_closure = self.getfixtureclosure( + names_closure, arg2fixturedefs = self.getfixtureclosure( node, initialnames, - arg2fixturedefs, + None, ignore_args=_get_direct_parametrize_args(node), ) return FuncFixtureInfo( @@ -1461,9 +1460,9 @@ class FixtureManager: self, parentnode: nodes.Node, initialnames: Tuple[str, ...], - arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]], + arg2fixturedefs: Union[Dict[str, Sequence[FixtureDef[Any]]], None], ignore_args: Sequence[str] = (), - ) -> List[str]: + ) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # initialnames containing function arguments, `usefixture` markers # and `autouse` fixtures as the initial set. As we have to visit all @@ -1474,6 +1473,8 @@ class FixtureManager: fixturenames_closure = initialnames + if arg2fixturedefs is None: + arg2fixturedefs = {} lastlen = -1 parentid = parentnode.nodeid while lastlen != len(fixturenames_closure): @@ -1498,7 +1499,10 @@ class FixtureManager: else: return fixturedefs[-1]._scope - return sorted(fixturenames_closure, key=sort_by_scope, reverse=True) + return ( + sorted(fixturenames_closure, key=sort_by_scope, reverse=True), + arg2fixturedefs, + ) def pytest_generate_tests(self, metafunc: "Metafunc") -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2e62d54b7..20a3d85e7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -381,21 +381,9 @@ del _EmptyClass # fmt: on -def prune_dependency_tree_if_test_is_dynamically_parametrized(metafunc): +def check_if_test_is_dynamically_parametrized(metafunc): if metafunc._calls: - # Dynamic direct parametrization may have shadowed some fixtures - # so make sure we update what the function really needs. Note that - # we didn't need to do this if only indirect dynamic parametrization - # had taken place, but anyway we did it as differentiating between direct - # and indirect requires a dirty hack. - definition = metafunc.definition - fixture_closure = definition.parent.session._fixturemanager.getfixtureclosure( - definition, - definition._fixtureinfo.initialnames, - definition._fixtureinfo.name2fixturedefs, - ignore_args=_get_direct_parametrize_args(definition) + ["request"], - ) - definition._fixtureinfo.names_closure[:] = fixture_closure + setattr(metafunc, "has_dynamic_parametrize", True) class PyCollector(PyobjMixin, nodes.Collector): @@ -502,7 +490,7 @@ class PyCollector(PyobjMixin, nodes.Collector): module=module, _ispytest=True, ) - methods = [prune_dependency_tree_if_test_is_dynamically_parametrized] + methods = [check_if_test_is_dynamically_parametrized] if hasattr(module, "pytest_generate_tests"): methods.append(module.pytest_generate_tests) if cls is not None and hasattr(cls, "pytest_generate_tests"): @@ -516,6 +504,20 @@ class PyCollector(PyobjMixin, nodes.Collector): yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: + if hasattr(metafunc, "has_dynamic_parametrize"): + # add_funcarg_pseudo_fixture_def may have shadowed some fixtures + # due to dynamic direct parametrization so make sure we update + # what the function really needs. Note that we didn't need to do this if + # only indirect dynamic parametrization had taken place, but anyway we did + # it as differentiating between direct and indirect requires a dirty hack. + fixture_closure, _ = fm.getfixtureclosure( + definition, + fixtureinfo.initialnames, + fixtureinfo.name2fixturedefs, + ignore_args=_get_direct_parametrize_args(definition), + ) + fixtureinfo.names_closure[:] = fixture_closure + for callspec in metafunc._calls: subname = f"{name}[{callspec.id}]" yield Function.from_parent( diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8dbac7514..ec0d44284 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4537,7 +4537,9 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None: @pytest.mark.xfail( - reason="arg2fixturedefs should get updated on dynamic parametrize. This gets solved by PR#11220" + reason="fixtureclosure should get updated before fixtures.py::pytest_generate_tests" + " and after modifying arg2fixturedefs when there's direct" + " dynamic parametrize. This gets solved by PR#11220" ) def test_fixture_info_after_dynamic_parametrize(pytester: Pytester) -> None: pytester.makeconftest( @@ -4575,7 +4577,7 @@ def test_fixture_info_after_dynamic_parametrize(pytester: Pytester) -> None: assert fixture2 in (4, 5) """ ) - res = pytester.inline_run("-s") + res = pytester.inline_run() res.assertoutcome(passed=2)