From bbf594903e303bde0c14319fe7d74e9ac993eedf Mon Sep 17 00:00:00 2001 From: Sadra Barikbin Date: Sun, 30 Jul 2023 01:46:21 +0330 Subject: [PATCH] Apply comments and and an improvement --- src/_pytest/fixtures.py | 40 ++++++++++++++++++-------------------- src/_pytest/python.py | 25 +++++++----------------- testing/python/fixtures.py | 7 +++++++ 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b7621a79b..9b45f1478 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1326,6 +1326,12 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]: return parametrize_argnames +def deduplicate_names(seq: Iterable[str]) -> Tuple[str, ...]: + """De-duplicate the sequence of names while keeping the original order.""" + # Ideally we would use a set, but it does not preserve insertion order. + return tuple(dict.fromkeys(seq)) + + class FixtureManager: """pytest fixture definitions and information is stored and managed from this class. @@ -1404,13 +1410,8 @@ class FixtureManager: usefixtures = tuple( arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args ) - initialnames = cast( - Tuple[str], - tuple( - dict.fromkeys( - tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames - ) - ), + initialnames = deduplicate_names( + tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames ) arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} @@ -1459,23 +1460,19 @@ class FixtureManager: def getfixtureclosure( self, parentnode: nodes.Node, - initialnames: Tuple[str], + initialnames: Tuple[str, ...], arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]], ignore_args: Sequence[str] = (), ) -> List[str]: # Collect the closure of all fixtures, starting with the given - # initialnames as the initial set. As we have to visit all - # factory definitions anyway, we also populate arg2fixturedefs - # mapping so that the caller can reuse it and does not have - # to re-discover fixturedefs again for each fixturename + # initialnames containing function arguments, `usefixture` markers + # and `autouse` fixtures as the initial set. As we have to visit all + # factory definitions anyway, we also populate arg2fixturedefs mapping + # for the args missing therein so that the caller can reuse it and does + # not have to re-discover fixturedefs again for each fixturename # (discovering matching fixtures for a given name/node is expensive). - fixturenames_closure = list(initialnames) - - def merge(otherlist: Iterable[str]) -> None: - for arg in otherlist: - if arg not in fixturenames_closure: - fixturenames_closure.append(arg) + fixturenames_closure = initialnames lastlen = -1 parentid = parentnode.nodeid @@ -1489,7 +1486,9 @@ class FixtureManager: if fixturedefs: arg2fixturedefs[argname] = fixturedefs if argname in arg2fixturedefs: - merge(arg2fixturedefs[argname][-1].argnames) + fixturenames_closure = deduplicate_names( + fixturenames_closure + arg2fixturedefs[argname][-1].argnames + ) def sort_by_scope(arg_name: str) -> Scope: try: @@ -1499,8 +1498,7 @@ class FixtureManager: else: return fixturedefs[-1]._scope - fixturenames_closure.sort(key=sort_by_scope, reverse=True) - return fixturenames_closure + return sorted(fixturenames_closure, key=sort_by_scope, reverse=True) 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 d8e6c23b7..2e62d54b7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -11,7 +11,6 @@ import warnings from collections import Counter from collections import defaultdict from functools import partial -from functools import wraps from pathlib import Path from typing import Any from typing import Callable @@ -382,12 +381,13 @@ del _EmptyClass # fmt: on -def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc): - metafunc.parametrize = metafunc._parametrize - del metafunc._parametrize - if metafunc.has_dynamic_parametrize: +def prune_dependency_tree_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. + # 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, @@ -396,7 +396,6 @@ def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc): ignore_args=_get_direct_parametrize_args(definition) + ["request"], ) definition._fixtureinfo.names_closure[:] = fixture_closure - del metafunc.has_dynamic_parametrize class PyCollector(PyobjMixin, nodes.Collector): @@ -503,22 +502,12 @@ class PyCollector(PyobjMixin, nodes.Collector): module=module, _ispytest=True, ) - methods = [unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree] + methods = [prune_dependency_tree_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"): methods.append(cls().pytest_generate_tests) - setattr(metafunc, "has_dynamic_parametrize", False) - - @wraps(metafunc.parametrize) - def set_has_dynamic_parametrize(*args, **kwargs): - setattr(metafunc, "has_dynamic_parametrize", True) - metafunc._parametrize(*args, **kwargs) # type: ignore[attr-defined] - - setattr(metafunc, "_parametrize", metafunc.parametrize) - setattr(metafunc, "parametrize", set_has_dynamic_parametrize) - # pytest_generate_tests impls call metafunc.parametrize() which fills # metafunc._calls, the outcome of the hook. self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 41dbb4549..8dbac7514 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4681,3 +4681,10 @@ def test_dont_recompute_dependency_tree_if_no_dynamic_parametrize(pytester: Pyte ) reprec = pytester.inline_run() reprec.assertoutcome(passed=5) + + +def test_deduplicate_names(pytester: Pytester) -> None: + items = fixtures.deduplicate_names("abacd") + assert items == ("a", "b", "c", "d") + items = fixtures.deduplicate_names(items + ("g", "f", "g", "e", "b")) + assert items == ("a", "b", "c", "d", "g", "f", "e")