From 120be26962021fa3e0393773ffad9ff465e3006e Mon Sep 17 00:00:00 2001 From: Sadra Barikbin Date: Tue, 5 Sep 2023 19:38:42 +0330 Subject: [PATCH] Improve solution Now we precisely prune only when there's dynamic direct parametrization. --- src/_pytest/python.py | 53 +++++++++++++++++++++----------------- testing/python/fixtures.py | 5 ---- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7318471e8..6465c8434 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -14,6 +14,7 @@ from functools import partial from pathlib import Path from typing import Any from typing import Callable +from typing import cast from typing import Dict from typing import final from typing import Generator @@ -381,11 +382,6 @@ del _EmptyClass # fmt: on -def check_if_test_is_dynamically_parametrized(metafunc): - if metafunc._calls: - setattr(metafunc, "has_dynamic_parametrize", True) - - class PyCollector(PyobjMixin, nodes.Collector): def funcnamefilter(self, name: str) -> bool: return self._matches_prefix_or_glob_option("python_functions", name) @@ -490,7 +486,16 @@ class PyCollector(PyobjMixin, nodes.Collector): module=module, _ispytest=True, ) - methods = [check_if_test_is_dynamically_parametrized] + + def prune_dependency_tree_if_test_is_directly_parametrized(metafunc: Metafunc): + # Direct (those with `indirect=False`) parametrizations taking place in + # module/class-specific `pytest_generate_tests` hooks, a.k.a dynamic direct + # parametrizations, may have shadowed some fixtures so make sure we update what + # the function really needs. + if metafunc.has_direct_parametrization: + metafunc.update_dependency_tree() + + methods = [prune_dependency_tree_if_test_is_directly_parametrized] if hasattr(module, "pytest_generate_tests"): methods.append(module.pytest_generate_tests) if cls is not None and hasattr(cls, "pytest_generate_tests"): @@ -503,23 +508,6 @@ class PyCollector(PyobjMixin, nodes.Collector): if not metafunc._calls: yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: - if hasattr(metafunc, "has_dynamic_parametrize"): - # Parametrizations takeing place in module/class-specific `pytest_generate_tests` - # hooks, a.k.a dynamic parametrizations, 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 i.e. with `indirect=True`, but anyway we did it as differentiating - # between direct and indirect requires a dirty hack. - fm = self.session._fixturemanager - 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( @@ -1232,6 +1220,9 @@ class Metafunc: # Result of parametrize(). self._calls: List[CallSpec2] = [] + # Whether it's ever been directly parametrized, i.e. with `indirect=False`. + self.has_direct_parametrization = False + def parametrize( self, argnames: Union[str, Sequence[str]], @@ -1380,6 +1371,7 @@ class Metafunc: for argname in argnames: if arg_directness[argname] == "indirect": continue + self.has_direct_parametrization = True if name2pseudofixturedef is not None and argname in name2pseudofixturedef: fixturedef = name2pseudofixturedef[argname] else: @@ -1549,6 +1541,21 @@ class Metafunc: pytrace=False, ) + def update_dependency_tree(self) -> None: + definition = self.definition + ( + fixture_closure, + _, + ) = cast( + nodes.Node, definition.parent + ).session._fixturemanager.getfixtureclosure( + definition, + definition._fixtureinfo.initialnames, + definition._fixtureinfo.name2fixturedefs, + ignore_args=_get_direct_parametrize_args(definition), + ) + definition._fixtureinfo.names_closure[:] = fixture_closure + def _find_parametrized_scope( argnames: Sequence[str], diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 192b17952..5d579b8af 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4534,11 +4534,6 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None: assert result.ret == ExitCode.TESTS_FAILED -@pytest.mark.xfail( - 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( """