Apply comments and and an improvement

This commit is contained in:
Sadra Barikbin 2023-07-30 01:46:21 +03:30
parent aa5d09deef
commit bbf594903e
3 changed files with 33 additions and 39 deletions

View File

@ -1326,6 +1326,12 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
return parametrize_argnames 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: class FixtureManager:
"""pytest fixture definitions and information is stored and managed """pytest fixture definitions and information is stored and managed
from this class. from this class.
@ -1404,14 +1410,9 @@ class FixtureManager:
usefixtures = tuple( usefixtures = tuple(
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
) )
initialnames = cast( initialnames = deduplicate_names(
Tuple[str],
tuple(
dict.fromkeys(
tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames
) )
),
)
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
names_closure = self.getfixtureclosure( names_closure = self.getfixtureclosure(
@ -1459,23 +1460,19 @@ class FixtureManager:
def getfixtureclosure( def getfixtureclosure(
self, self,
parentnode: nodes.Node, parentnode: nodes.Node,
initialnames: Tuple[str], initialnames: Tuple[str, ...],
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]], arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]],
ignore_args: Sequence[str] = (), ignore_args: Sequence[str] = (),
) -> List[str]: ) -> List[str]:
# Collect the closure of all fixtures, starting with the given # Collect the closure of all fixtures, starting with the given
# initialnames as the initial set. As we have to visit all # initialnames containing function arguments, `usefixture` markers
# factory definitions anyway, we also populate arg2fixturedefs # and `autouse` fixtures as the initial set. As we have to visit all
# mapping so that the caller can reuse it and does not have # factory definitions anyway, we also populate arg2fixturedefs mapping
# to re-discover fixturedefs again for each fixturename # 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). # (discovering matching fixtures for a given name/node is expensive).
fixturenames_closure = list(initialnames) fixturenames_closure = initialnames
def merge(otherlist: Iterable[str]) -> None:
for arg in otherlist:
if arg not in fixturenames_closure:
fixturenames_closure.append(arg)
lastlen = -1 lastlen = -1
parentid = parentnode.nodeid parentid = parentnode.nodeid
@ -1489,7 +1486,9 @@ class FixtureManager:
if fixturedefs: if fixturedefs:
arg2fixturedefs[argname] = fixturedefs arg2fixturedefs[argname] = fixturedefs
if argname in arg2fixturedefs: 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: def sort_by_scope(arg_name: str) -> Scope:
try: try:
@ -1499,8 +1498,7 @@ class FixtureManager:
else: else:
return fixturedefs[-1]._scope return fixturedefs[-1]._scope
fixturenames_closure.sort(key=sort_by_scope, reverse=True) return sorted(fixturenames_closure, key=sort_by_scope, reverse=True)
return fixturenames_closure
def pytest_generate_tests(self, metafunc: "Metafunc") -> None: def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
"""Generate new tests based on parametrized fixtures used by the given metafunc""" """Generate new tests based on parametrized fixtures used by the given metafunc"""

View File

@ -11,7 +11,6 @@ import warnings
from collections import Counter from collections import Counter
from collections import defaultdict from collections import defaultdict
from functools import partial from functools import partial
from functools import wraps
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
@ -382,12 +381,13 @@ del _EmptyClass
# fmt: on # fmt: on
def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc): def prune_dependency_tree_if_test_is_dynamically_parametrized(metafunc):
metafunc.parametrize = metafunc._parametrize if metafunc._calls:
del metafunc._parametrize
if metafunc.has_dynamic_parametrize:
# Dynamic direct parametrization may have shadowed some fixtures # 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 definition = metafunc.definition
fixture_closure = definition.parent.session._fixturemanager.getfixtureclosure( fixture_closure = definition.parent.session._fixturemanager.getfixtureclosure(
definition, definition,
@ -396,7 +396,6 @@ def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc):
ignore_args=_get_direct_parametrize_args(definition) + ["request"], ignore_args=_get_direct_parametrize_args(definition) + ["request"],
) )
definition._fixtureinfo.names_closure[:] = fixture_closure definition._fixtureinfo.names_closure[:] = fixture_closure
del metafunc.has_dynamic_parametrize
class PyCollector(PyobjMixin, nodes.Collector): class PyCollector(PyobjMixin, nodes.Collector):
@ -503,22 +502,12 @@ class PyCollector(PyobjMixin, nodes.Collector):
module=module, module=module,
_ispytest=True, _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"): 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"):
methods.append(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 # pytest_generate_tests impls call metafunc.parametrize() which fills
# metafunc._calls, the outcome of the hook. # metafunc._calls, the outcome of the hook.
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))

View File

@ -4681,3 +4681,10 @@ def test_dont_recompute_dependency_tree_if_no_dynamic_parametrize(pytester: Pyte
) )
reprec = pytester.inline_run() reprec = pytester.inline_run()
reprec.assertoutcome(passed=5) 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")