Skip to content

Commit 5bd5b80

Browse files
committed
nodes: add Node.iterparents() function
This is a useful addition to the existing `listchain`. While `listchain` returns top-to-bottom, `iterparents` is bottom-to-top and doesn't require an internal full iteration + `reverse`.
1 parent b1c4308 commit 5bd5b80

File tree

4 files changed

+30
-38
lines changed

4 files changed

+30
-38
lines changed

changelog/11801.improvement.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added the :func:`iterparents() <_pytest.nodes.Node.iterparents>` helper method on nodes.
2+
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.

src/_pytest/fixtures.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,16 @@ def pytest_sessionstart(session: "Session") -> None:
116116
def get_scope_package(
117117
node: nodes.Item,
118118
fixturedef: "FixtureDef[object]",
119-
) -> Optional[Union[nodes.Item, nodes.Collector]]:
119+
) -> Optional[nodes.Node]:
120120
from _pytest.python import Package
121121

122-
current: Optional[Union[nodes.Item, nodes.Collector]] = node
123-
while current and (
124-
not isinstance(current, Package) or current.nodeid != fixturedef.baseid
125-
):
126-
current = current.parent # type: ignore[assignment]
127-
if current is None:
128-
return node.session
129-
return current
122+
for parent in node.iterparents():
123+
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
124+
return parent
125+
return node.session
130126

131127

132-
def get_scope_node(
133-
node: nodes.Node, scope: Scope
134-
) -> Optional[Union[nodes.Item, nodes.Collector]]:
128+
def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]:
135129
import _pytest.python
136130

137131
if scope is Scope.Function:
@@ -738,7 +732,7 @@ def node(self):
738732
scope = self._scope
739733
if scope is Scope.Function:
740734
# This might also be a non-function Item despite its attribute name.
741-
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
735+
node: Optional[nodes.Node] = self._pyfuncitem
742736
elif scope is Scope.Package:
743737
node = get_scope_package(self._pyfuncitem, self._fixturedef)
744738
else:
@@ -1513,7 +1507,7 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
15131507

15141508
def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
15151509
"""Return the names of autouse fixtures applicable to node."""
1516-
for parentnode in reversed(list(nodes.iterparentnodes(node))):
1510+
for parentnode in node.listchain():
15171511
basenames = self._nodeid_autousenames.get(parentnode.nodeid)
15181512
if basenames:
15191513
yield from basenames
@@ -1781,7 +1775,7 @@ def getfixturedefs(
17811775
def _matchfactories(
17821776
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
17831777
) -> Iterator[FixtureDef[Any]]:
1784-
parentnodeids = {n.nodeid for n in nodes.iterparentnodes(node)}
1778+
parentnodeids = {n.nodeid for n in node.iterparents()}
17851779
for fixturedef in fixturedefs:
17861780
if fixturedef.baseid in parentnodeids:
17871781
yield fixturedef

src/_pytest/nodes.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,6 @@
4949
tracebackcutdir = Path(_pytest.__file__).parent
5050

5151

52-
def iterparentnodes(node: "Node") -> Iterator["Node"]:
53-
"""Return the parent nodes, including the node itself, from the node
54-
upwards."""
55-
parent: Optional[Node] = node
56-
while parent is not None:
57-
yield parent
58-
parent = parent.parent
59-
60-
6152
_NodeType = TypeVar("_NodeType", bound="Node")
6253

6354

@@ -265,12 +256,20 @@ def setup(self) -> None:
265256
def teardown(self) -> None:
266257
pass
267258

268-
def listchain(self) -> List["Node"]:
269-
"""Return list of all parent collectors up to self, starting from
270-
the root of collection tree.
259+
def iterparents(self) -> Iterator["Node"]:
260+
"""Iterate over all parent collectors starting from and including self
261+
up to the root of the collection tree.
271262
272-
:returns: The nodes.
263+
.. versionadded:: 8.1
273264
"""
265+
parent: Optional[Node] = self
266+
while parent is not None:
267+
yield parent
268+
parent = parent.parent
269+
270+
def listchain(self) -> List["Node"]:
271+
"""Return a list of all parent collectors starting from the root of the
272+
collection tree down to and including self."""
274273
chain = []
275274
item: Optional[Node] = self
276275
while item is not None:
@@ -319,7 +318,7 @@ def iter_markers_with_node(
319318
:param name: If given, filter the results by the name attribute.
320319
:returns: An iterator of (node, mark) tuples.
321320
"""
322-
for node in reversed(self.listchain()):
321+
for node in self.iterparents():
323322
for mark in node.own_markers:
324323
if name is None or getattr(mark, "name", None) == name:
325324
yield node, mark
@@ -363,17 +362,16 @@ def addfinalizer(self, fin: Callable[[], object]) -> None:
363362
self.session._setupstate.addfinalizer(fin, self)
364363

365364
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
366-
"""Get the next parent node (including self) which is an instance of
365+
"""Get the closest parent node (including self) which is an instance of
367366
the given class.
368367
369368
:param cls: The node class to search for.
370369
:returns: The node, if found.
371370
"""
372-
current: Optional[Node] = self
373-
while current and not isinstance(current, cls):
374-
current = current.parent
375-
assert current is None or isinstance(current, cls)
376-
return current
371+
for node in self.iterparents():
372+
if isinstance(node, cls):
373+
return node
374+
return None
377375

378376
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
379377
return excinfo.traceback

src/_pytest/python.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,10 +333,8 @@ def _getobj(self):
333333

334334
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
335335
"""Return Python path relative to the containing module."""
336-
chain = self.listchain()
337-
chain.reverse()
338336
parts = []
339-
for node in chain:
337+
for node in self.iterparents():
340338
name = node.name
341339
if isinstance(node, Module):
342340
name = os.path.splitext(name)[0]

0 commit comments

Comments
 (0)