Skip to content

Commit 65e6e39

Browse files
authored
Merge pull request #7931 from bluetech/xunit-quadratic-2
fixtures: fix quadratic behavior in the number of autouse fixtures
2 parents f7d4f45 + 470ea50 commit 65e6e39

File tree

5 files changed

+63
-69
lines changed

5 files changed

+63
-69
lines changed

changelog/4824.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures.

src/_pytest/fixtures.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,9 +1412,10 @@ def __init__(self, session: "Session") -> None:
14121412
self.config: Config = session.config
14131413
self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
14141414
self._holderobjseen: Set[object] = set()
1415-
self._nodeid_and_autousenames: List[Tuple[str, List[str]]] = [
1416-
("", self.config.getini("usefixtures"))
1417-
]
1415+
# A mapping from a nodeid to a list of autouse fixtures it defines.
1416+
self._nodeid_autousenames: Dict[str, List[str]] = {
1417+
"": self.config.getini("usefixtures"),
1418+
}
14181419
session.config.pluginmanager.register(self, "funcmanage")
14191420

14201421
def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]:
@@ -1476,18 +1477,12 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
14761477

14771478
self.parsefactories(plugin, nodeid)
14781479

1479-
def _getautousenames(self, nodeid: str) -> List[str]:
1480-
"""Return a list of fixture names to be used."""
1481-
autousenames: List[str] = []
1482-
for baseid, basenames in self._nodeid_and_autousenames:
1483-
if nodeid.startswith(baseid):
1484-
if baseid:
1485-
i = len(baseid)
1486-
nextchar = nodeid[i : i + 1]
1487-
if nextchar and nextchar not in ":/":
1488-
continue
1489-
autousenames.extend(basenames)
1490-
return autousenames
1480+
def _getautousenames(self, nodeid: str) -> Iterator[str]:
1481+
"""Return the names of autouse fixtures applicable to nodeid."""
1482+
for parentnodeid in nodes.iterparentnodeids(nodeid):
1483+
basenames = self._nodeid_autousenames.get(parentnodeid)
1484+
if basenames:
1485+
yield from basenames
14911486

14921487
def getfixtureclosure(
14931488
self,
@@ -1503,7 +1498,7 @@ def getfixtureclosure(
15031498
# (discovering matching fixtures for a given name/node is expensive).
15041499

15051500
parentid = parentnode.nodeid
1506-
fixturenames_closure = self._getautousenames(parentid)
1501+
fixturenames_closure = list(self._getautousenames(parentid))
15071502

15081503
def merge(otherlist: Iterable[str]) -> None:
15091504
for arg in otherlist:
@@ -1648,7 +1643,7 @@ def parsefactories(
16481643
autousenames.append(name)
16491644

16501645
if autousenames:
1651-
self._nodeid_and_autousenames.append((nodeid or "", autousenames))
1646+
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
16521647

16531648
def getfixturedefs(
16541649
self, argname: str, nodeid: str
@@ -1668,6 +1663,7 @@ def getfixturedefs(
16681663
def _matchfactories(
16691664
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
16701665
) -> Iterator[FixtureDef[Any]]:
1666+
parentnodeids = set(nodes.iterparentnodeids(nodeid))
16711667
for fixturedef in fixturedefs:
1672-
if nodes.ischildnode(fixturedef.baseid, nodeid):
1668+
if fixturedef.baseid in parentnodeids:
16731669
yield fixturedef

src/_pytest/nodes.py

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import warnings
3-
from functools import lru_cache
43
from pathlib import Path
54
from typing import Callable
65
from typing import Iterable
@@ -44,46 +43,39 @@
4443
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
4544

4645

47-
@lru_cache(maxsize=None)
48-
def _splitnode(nodeid: str) -> Tuple[str, ...]:
49-
"""Split a nodeid into constituent 'parts'.
46+
def iterparentnodeids(nodeid: str) -> Iterator[str]:
47+
"""Return the parent node IDs of a given node ID, inclusive.
5048
51-
Node IDs are strings, and can be things like:
52-
''
53-
'testing/code'
54-
'testing/code/test_excinfo.py'
55-
'testing/code/test_excinfo.py::TestFormattedExcinfo'
49+
For the node ID
5650
57-
Return values are lists e.g.
58-
[]
59-
['testing', 'code']
60-
['testing', 'code', 'test_excinfo.py']
61-
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
62-
"""
63-
if nodeid == "":
64-
# If there is no root node at all, return an empty list so the caller's
65-
# logic can remain sane.
66-
return ()
67-
parts = nodeid.split(SEP)
68-
# Replace single last element 'test_foo.py::Bar' with multiple elements
69-
# 'test_foo.py', 'Bar'.
70-
parts[-1:] = parts[-1].split("::")
71-
# Convert parts into a tuple to avoid possible errors with caching of a
72-
# mutable type.
73-
return tuple(parts)
74-
75-
76-
def ischildnode(baseid: str, nodeid: str) -> bool:
77-
"""Return True if the nodeid is a child node of the baseid.
78-
79-
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
80-
but not of 'foo/blorp'.
51+
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
52+
53+
the result would be
54+
55+
""
56+
"testing"
57+
"testing/code"
58+
"testing/code/test_excinfo.py"
59+
"testing/code/test_excinfo.py::TestFormattedExcinfo"
60+
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
61+
62+
Note that :: parts are only considered at the last / component.
8163
"""
82-
base_parts = _splitnode(baseid)
83-
node_parts = _splitnode(nodeid)
84-
if len(node_parts) < len(base_parts):
85-
return False
86-
return node_parts[: len(base_parts)] == base_parts
64+
pos = 0
65+
sep = SEP
66+
yield ""
67+
while True:
68+
at = nodeid.find(sep, pos)
69+
if at == -1 and sep == SEP:
70+
sep = "::"
71+
elif at == -1:
72+
if nodeid:
73+
yield nodeid
74+
break
75+
else:
76+
if at:
77+
yield nodeid[:at]
78+
pos = at + len(sep)
8779

8880

8981
_NodeType = TypeVar("_NodeType", bound="Node")

testing/python/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1710,7 +1710,7 @@ def test_parsefactories_conftest(self, testdir):
17101710
"""
17111711
from _pytest.pytester import get_public_names
17121712
def test_check_setup(item, fm):
1713-
autousenames = fm._getautousenames(item.nodeid)
1713+
autousenames = list(fm._getautousenames(item.nodeid))
17141714
assert len(get_public_names(autousenames)) == 2
17151715
assert "perfunction2" in autousenames
17161716
assert "perfunction" in autousenames

testing/test_nodes.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import List
2+
13
import py
24

35
import pytest
@@ -6,21 +8,24 @@
68

79

810
@pytest.mark.parametrize(
9-
"baseid, nodeid, expected",
11+
("nodeid", "expected"),
1012
(
11-
("", "", True),
12-
("", "foo", True),
13-
("", "foo/bar", True),
14-
("", "foo/bar::TestBaz", True),
15-
("foo", "food", False),
16-
("foo/bar::TestBaz", "foo/bar", False),
17-
("foo/bar::TestBaz", "foo/bar::TestBop", False),
18-
("foo/bar", "foo/bar::TestBop", True),
13+
("", [""]),
14+
("a", ["", "a"]),
15+
("aa/b", ["", "aa", "aa/b"]),
16+
("a/b/c", ["", "a", "a/b", "a/b/c"]),
17+
("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]),
18+
("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]),
19+
# :: considered only at the last component.
20+
("::xx", ["", "::xx"]),
21+
("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c::D", "a/b/c::D/d", "a/b/c::D/d::e"]),
22+
# : alone is not a separator.
23+
("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]),
1924
),
2025
)
21-
def test_ischildnode(baseid: str, nodeid: str, expected: bool) -> None:
22-
result = nodes.ischildnode(baseid, nodeid)
23-
assert result is expected
26+
def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None:
27+
result = list(nodes.iterparentnodeids(nodeid))
28+
assert result == expected
2429

2530

2631
def test_node_from_parent_disallowed_arguments() -> None:

0 commit comments

Comments
 (0)