From 48b03956482e2082191bb58d707f3c004f15aa68 Mon Sep 17 00:00:00 2001 From: Sadra Barikbin Date: Mon, 4 Sep 2023 23:44:49 +0300 Subject: [PATCH] fixtures: clean up getfixtureclosure() Some code cleanups - no functional changes. --- src/_pytest/fixtures.py | 51 +++++++++++++++++++++----------------- testing/python/fixtures.py | 8 ++++++ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 205176f57..80b965b81 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1402,6 +1402,12 @@ def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]: return parametrize_argnames +def deduplicate_names(*seqs: 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(name for seq in seqs for name in seq)) + + class FixtureManager: """pytest fixture definitions and information is stored and managed from this class. @@ -1476,14 +1482,18 @@ class FixtureManager: argnames = getfuncargnames(func, name=node.name, cls=cls) else: argnames = () + usefixturesnames = self._getusefixturesnames(node) + autousenames = self._getautousenames(node.nodeid) + initialnames = deduplicate_names(autousenames, usefixturesnames, argnames) - usefixtures = tuple( - arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args - ) - initialnames = usefixtures + argnames - initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure( - initialnames, node, ignore_args=_get_direct_parametrize_args(node) + direct_parametrize_args = _get_direct_parametrize_args(node) + + names_closure, arg2fixturedefs = self.getfixtureclosure( + parentnode=node, + initialnames=initialnames, + ignore_args=direct_parametrize_args, ) + return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: @@ -1515,12 +1525,17 @@ class FixtureManager: if basenames: yield from basenames + def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: + """Return the names of usefixtures fixtures applicable to node.""" + for mark in node.iter_markers(name="usefixtures"): + yield from mark.args + def getfixtureclosure( self, - fixturenames: Tuple[str, ...], parentnode: nodes.Node, + initialnames: Tuple[str, ...], ignore_args: AbstractSet[str], - ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: + ) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1529,19 +1544,7 @@ class FixtureManager: # (discovering matching fixtures for a given name/node is expensive). parentid = parentnode.nodeid - fixturenames_closure = list(self._getautousenames(parentid)) - - def merge(otherlist: Iterable[str]) -> None: - for arg in otherlist: - if arg not in fixturenames_closure: - fixturenames_closure.append(arg) - - merge(fixturenames) - - # At this point, fixturenames_closure contains what we call "initialnames", - # which is a set of fixturenames the function immediately requests. We - # need to return it as well, so save this. - initialnames = tuple(fixturenames_closure) + fixturenames_closure = list(initialnames) arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} lastlen = -1 @@ -1555,7 +1558,9 @@ class FixtureManager: fixturedefs = self.getfixturedefs(argname, parentid) if fixturedefs: arg2fixturedefs[argname] = fixturedefs - merge(fixturedefs[-1].argnames) + for arg in fixturedefs[-1].argnames: + if arg not in fixturenames_closure: + fixturenames_closure.append(arg) def sort_by_scope(arg_name: str) -> Scope: try: @@ -1566,7 +1571,7 @@ class FixtureManager: return fixturedefs[-1]._scope fixturenames_closure.sort(key=sort_by_scope, reverse=True) - return initialnames, fixturenames_closure, arg2fixturedefs + return fixturenames_closure, arg2fixturedefs def pytest_generate_tests(self, metafunc: "Metafunc") -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a8f36cb9f..775056a8e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6,6 +6,7 @@ from pathlib import Path import pytest from _pytest.compat import getfuncargnames from _pytest.config import ExitCode +from _pytest.fixtures import deduplicate_names from _pytest.fixtures import TopRequest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import get_public_names @@ -4531,3 +4532,10 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None: result.assert_outcomes(errors=1) result.stdout.fnmatch_lines([expected]) assert result.ret == ExitCode.TESTS_FAILED + + +def test_deduplicate_names() -> None: + items = deduplicate_names("abacd") + assert items == ("a", "b", "c", "d") + items = deduplicate_names(items + ("g", "f", "g", "e", "b")) + assert items == ("a", "b", "c", "d", "g", "f", "e")