Skip to content

Commit d508df7

Browse files
Apply comments and and an improvement
1 parent b0aaca4 commit d508df7

File tree

3 files changed

+33
-39
lines changed

3 files changed

+33
-39
lines changed

src/_pytest/fixtures.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,12 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
14041404
return parametrize_argnames
14051405

14061406

1407+
def deduplicate_names(seq: Iterable[str]) -> Tuple[str, ...]:
1408+
"""De-duplicate the sequence of names while keeping the original order."""
1409+
# Ideally we would use a set, but it does not preserve insertion order.
1410+
return tuple(dict.fromkeys(seq))
1411+
1412+
14071413
class FixtureManager:
14081414
"""pytest fixture definitions and information is stored and managed
14091415
from this class.
@@ -1482,13 +1488,8 @@ def getfixtureinfo(
14821488
usefixtures = tuple(
14831489
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
14841490
)
1485-
initialnames = cast(
1486-
Tuple[str],
1487-
tuple(
1488-
dict.fromkeys(
1489-
tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames
1490-
)
1491-
),
1491+
initialnames = deduplicate_names(
1492+
tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames
14921493
)
14931494

14941495
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
@@ -1537,23 +1538,19 @@ def _getautousenames(self, nodeid: str) -> Iterator[str]:
15371538
def getfixtureclosure(
15381539
self,
15391540
parentnode: nodes.Node,
1540-
initialnames: Tuple[str],
1541+
initialnames: Tuple[str, ...],
15411542
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]],
15421543
ignore_args: Sequence[str] = (),
15431544
) -> List[str]:
15441545
# Collect the closure of all fixtures, starting with the given
1545-
# initialnames as the initial set. As we have to visit all
1546-
# factory definitions anyway, we also populate arg2fixturedefs
1547-
# mapping so that the caller can reuse it and does not have
1548-
# to re-discover fixturedefs again for each fixturename
1546+
# initialnames containing function arguments, `usefixture` markers
1547+
# and `autouse` fixtures as the initial set. As we have to visit all
1548+
# factory definitions anyway, we also populate arg2fixturedefs mapping
1549+
# for the args missing therein so that the caller can reuse it and does
1550+
# not have to re-discover fixturedefs again for each fixturename
15491551
# (discovering matching fixtures for a given name/node is expensive).
15501552

1551-
fixturenames_closure = list(initialnames)
1552-
1553-
def merge(otherlist: Iterable[str]) -> None:
1554-
for arg in otherlist:
1555-
if arg not in fixturenames_closure:
1556-
fixturenames_closure.append(arg)
1553+
fixturenames_closure = initialnames
15571554

15581555
lastlen = -1
15591556
parentid = parentnode.nodeid
@@ -1567,7 +1564,9 @@ def merge(otherlist: Iterable[str]) -> None:
15671564
if fixturedefs:
15681565
arg2fixturedefs[argname] = fixturedefs
15691566
if argname in arg2fixturedefs:
1570-
merge(arg2fixturedefs[argname][-1].argnames)
1567+
fixturenames_closure = deduplicate_names(
1568+
fixturenames_closure + arg2fixturedefs[argname][-1].argnames
1569+
)
15711570

15721571
def sort_by_scope(arg_name: str) -> Scope:
15731572
try:
@@ -1577,8 +1576,7 @@ def sort_by_scope(arg_name: str) -> Scope:
15771576
else:
15781577
return fixturedefs[-1]._scope
15791578

1580-
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
1581-
return fixturenames_closure
1579+
return sorted(fixturenames_closure, key=sort_by_scope, reverse=True)
15821580

15831581
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
15841582
"""Generate new tests based on parametrized fixtures used by the given metafunc"""

src/_pytest/python.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from collections import Counter
1212
from collections import defaultdict
1313
from functools import partial
14-
from functools import wraps
1514
from pathlib import Path
1615
from typing import Any
1716
from typing import Callable
@@ -379,12 +378,13 @@ class _EmptyClass: pass # noqa: E701
379378
# fmt: on
380379

381380

382-
def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc):
383-
metafunc.parametrize = metafunc._parametrize
384-
del metafunc._parametrize
385-
if metafunc.has_dynamic_parametrize:
381+
def prune_dependency_tree_if_test_is_dynamically_parametrized(metafunc):
382+
if metafunc._calls:
386383
# Dynamic direct parametrization may have shadowed some fixtures
387-
# so make sure we update what the function really needs.
384+
# so make sure we update what the function really needs. Note that
385+
# we didn't need to do this if only indirect dynamic parametrization
386+
# had taken place, but anyway we did it as differentiating between direct
387+
# and indirect requires a dirty hack.
388388
definition = metafunc.definition
389389
fixture_closure = definition.parent.session._fixturemanager.getfixtureclosure(
390390
definition,
@@ -393,7 +393,6 @@ def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc):
393393
ignore_args=_get_direct_parametrize_args(definition) + ["request"],
394394
)
395395
definition._fixtureinfo.names_closure[:] = fixture_closure
396-
del metafunc.has_dynamic_parametrize
397396

398397

399398
class PyCollector(PyobjMixin, nodes.Collector):
@@ -500,22 +499,12 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
500499
module=module,
501500
_ispytest=True,
502501
)
503-
methods = [unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree]
502+
methods = [prune_dependency_tree_if_test_is_dynamically_parametrized]
504503
if hasattr(module, "pytest_generate_tests"):
505504
methods.append(module.pytest_generate_tests)
506505
if cls is not None and hasattr(cls, "pytest_generate_tests"):
507506
methods.append(cls().pytest_generate_tests)
508507

509-
setattr(metafunc, "has_dynamic_parametrize", False)
510-
511-
@wraps(metafunc.parametrize)
512-
def set_has_dynamic_parametrize(*args, **kwargs):
513-
setattr(metafunc, "has_dynamic_parametrize", True)
514-
metafunc._parametrize(*args, **kwargs) # type: ignore[attr-defined]
515-
516-
setattr(metafunc, "_parametrize", metafunc.parametrize)
517-
setattr(metafunc, "parametrize", set_has_dynamic_parametrize)
518-
519508
# pytest_generate_tests impls call metafunc.parametrize() which fills
520509
# metafunc._calls, the outcome of the hook.
521510
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))

testing/python/fixtures.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4683,3 +4683,10 @@ def test(fm):
46834683
)
46844684
reprec = pytester.inline_run()
46854685
reprec.assertoutcome(passed=5)
4686+
4687+
4688+
def test_deduplicate_names(pytester: Pytester) -> None:
4689+
items = fixtures.deduplicate_names("abacd")
4690+
assert items == ("a", "b", "c", "d")
4691+
items = fixtures.deduplicate_names(items + ("g", "f", "g", "e", "b"))
4692+
assert items == ("a", "b", "c", "d", "g", "f", "e")

0 commit comments

Comments
 (0)