Improve solution

Now we precisely prune only when there's dynamic direct parametrization.
This commit is contained in:
Sadra Barikbin 2023-09-05 19:38:42 +03:30
parent 57ad1e86ca
commit 120be26962
2 changed files with 30 additions and 28 deletions

View File

@ -14,6 +14,7 @@ from functools import partial
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast
from typing import Dict from typing import Dict
from typing import final from typing import final
from typing import Generator from typing import Generator
@ -381,11 +382,6 @@ del _EmptyClass
# fmt: on # fmt: on
def check_if_test_is_dynamically_parametrized(metafunc):
if metafunc._calls:
setattr(metafunc, "has_dynamic_parametrize", True)
class PyCollector(PyobjMixin, nodes.Collector): class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name: str) -> bool: def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name) return self._matches_prefix_or_glob_option("python_functions", name)
@ -490,7 +486,16 @@ class PyCollector(PyobjMixin, nodes.Collector):
module=module, module=module,
_ispytest=True, _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"): if hasattr(module, "pytest_generate_tests"):
methods.append(module.pytest_generate_tests) methods.append(module.pytest_generate_tests)
if cls is not None and hasattr(cls, "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: if not metafunc._calls:
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
else: 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: for callspec in metafunc._calls:
subname = f"{name}[{callspec.id}]" subname = f"{name}[{callspec.id}]"
yield Function.from_parent( yield Function.from_parent(
@ -1232,6 +1220,9 @@ class Metafunc:
# Result of parametrize(). # Result of parametrize().
self._calls: List[CallSpec2] = [] self._calls: List[CallSpec2] = []
# Whether it's ever been directly parametrized, i.e. with `indirect=False`.
self.has_direct_parametrization = False
def parametrize( def parametrize(
self, self,
argnames: Union[str, Sequence[str]], argnames: Union[str, Sequence[str]],
@ -1380,6 +1371,7 @@ class Metafunc:
for argname in argnames: for argname in argnames:
if arg_directness[argname] == "indirect": if arg_directness[argname] == "indirect":
continue continue
self.has_direct_parametrization = True
if name2pseudofixturedef is not None and argname in name2pseudofixturedef: if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
fixturedef = name2pseudofixturedef[argname] fixturedef = name2pseudofixturedef[argname]
else: else:
@ -1549,6 +1541,21 @@ class Metafunc:
pytrace=False, 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( def _find_parametrized_scope(
argnames: Sequence[str], argnames: Sequence[str],

View File

@ -4534,11 +4534,6 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None:
assert result.ret == ExitCode.TESTS_FAILED 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: def test_fixture_info_after_dynamic_parametrize(pytester: Pytester) -> None:
pytester.makeconftest( pytester.makeconftest(
""" """