fixtures: use a faster replacement for ischildnode

ischildnode can be quite hot in some cases involving many fixtures.
However it is always used in a way that the nodeid is constant and the
baseid is iterated. So we can save work by pre-computing the parents of
the nodeid and use a simple containment test.

The `_getautousenames` function has the same stuff open-coded, so change
it to use the new function as well.
This commit is contained in:
Ran Benita 2020-10-24 02:09:28 +03:00
parent 0b14350f23
commit aa0e2d654f
3 changed files with 51 additions and 57 deletions

View File

@ -1478,14 +1478,10 @@ class FixtureManager:
def _getautousenames(self, nodeid: str) -> List[str]:
"""Return a list of fixture names to be used."""
parentnodeids = set(nodes.iterparentnodeids(nodeid))
autousenames: List[str] = []
for baseid, basenames in self._nodeid_and_autousenames:
if nodeid.startswith(baseid):
if baseid:
i = len(baseid)
nextchar = nodeid[i : i + 1]
if nextchar and nextchar not in ":/":
continue
if baseid in parentnodeids:
autousenames.extend(basenames)
return autousenames
@ -1668,6 +1664,7 @@ class FixtureManager:
def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
) -> Iterator[FixtureDef[Any]]:
parentnodeids = set(nodes.iterparentnodeids(nodeid))
for fixturedef in fixturedefs:
if nodes.ischildnode(fixturedef.baseid, nodeid):
if fixturedef.baseid in parentnodeids:
yield fixturedef

View File

@ -1,6 +1,5 @@
import os
import warnings
from functools import lru_cache
from pathlib import Path
from typing import Callable
from typing import Iterable
@ -44,46 +43,39 @@ SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@lru_cache(maxsize=None)
def _splitnode(nodeid: str) -> Tuple[str, ...]:
"""Split a nodeid into constituent 'parts'.
def iterparentnodeids(nodeid: str) -> Iterator[str]:
"""Return the parent node IDs of a given node ID, inclusive.
Node IDs are strings, and can be things like:
''
'testing/code'
'testing/code/test_excinfo.py'
'testing/code/test_excinfo.py::TestFormattedExcinfo'
For the node ID
Return values are lists e.g.
[]
['testing', 'code']
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
the result would be
""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that :: parts are only considered at the last / component.
"""
if nodeid == "":
# If there is no root node at all, return an empty list so the caller's
# logic can remain sane.
return ()
parts = nodeid.split(SEP)
# Replace single last element 'test_foo.py::Bar' with multiple elements
# 'test_foo.py', 'Bar'.
parts[-1:] = parts[-1].split("::")
# Convert parts into a tuple to avoid possible errors with caching of a
# mutable type.
return tuple(parts)
def ischildnode(baseid: str, nodeid: str) -> bool:
"""Return True if the nodeid is a child node of the baseid.
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
but not of 'foo/blorp'.
"""
base_parts = _splitnode(baseid)
node_parts = _splitnode(nodeid)
if len(node_parts) < len(base_parts):
return False
return node_parts[: len(base_parts)] == base_parts
pos = 0
sep = SEP
yield ""
while True:
at = nodeid.find(sep, pos)
if at == -1 and sep == SEP:
sep = "::"
elif at == -1:
if nodeid:
yield nodeid
break
else:
if at:
yield nodeid[:at]
pos = at + len(sep)
_NodeType = TypeVar("_NodeType", bound="Node")

View File

@ -1,3 +1,5 @@
from typing import List
import py
import pytest
@ -6,21 +8,24 @@ from _pytest.pytester import Testdir
@pytest.mark.parametrize(
"baseid, nodeid, expected",
("nodeid", "expected"),
(
("", "", True),
("", "foo", True),
("", "foo/bar", True),
("", "foo/bar::TestBaz", True),
("foo", "food", False),
("foo/bar::TestBaz", "foo/bar", False),
("foo/bar::TestBaz", "foo/bar::TestBop", False),
("foo/bar", "foo/bar::TestBop", True),
("", [""]),
("a", ["", "a"]),
("aa/b", ["", "aa", "aa/b"]),
("a/b/c", ["", "a", "a/b", "a/b/c"]),
("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]),
("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]),
# :: considered only at the last component.
("::xx", ["", "::xx"]),
("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c::D", "a/b/c::D/d", "a/b/c::D/d::e"]),
# : alone is not a separator.
("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]),
),
)
def test_ischildnode(baseid: str, nodeid: str, expected: bool) -> None:
result = nodes.ischildnode(baseid, nodeid)
assert result is expected
def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None:
result = list(nodes.iterparentnodeids(nodeid))
assert result == expected
def test_node_from_parent_disallowed_arguments() -> None: