Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/nodes.py: 64%
333 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1import os
2import warnings
3from inspect import signature
4from pathlib import Path
5from typing import Any
6from typing import Callable
7from typing import cast
8from typing import Iterable
9from typing import Iterator
10from typing import List
11from typing import MutableMapping
12from typing import Optional
13from typing import overload
14from typing import Set
15from typing import Tuple
16from typing import Type
17from typing import TYPE_CHECKING
18from typing import TypeVar
19from typing import Union
21import _pytest._code
22from _pytest._code import getfslineno
23from _pytest._code.code import ExceptionInfo
24from _pytest._code.code import TerminalRepr
25from _pytest.compat import cached_property
26from _pytest.compat import LEGACY_PATH
27from _pytest.config import Config
28from _pytest.config import ConftestImportFailure
29from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
30from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
31from _pytest.mark.structures import Mark
32from _pytest.mark.structures import MarkDecorator
33from _pytest.mark.structures import NodeKeywords
34from _pytest.outcomes import fail
35from _pytest.pathlib import absolutepath
36from _pytest.pathlib import commonpath
37from _pytest.stash import Stash
38from _pytest.warning_types import PytestWarning
40if TYPE_CHECKING:
41 # Imported here due to circular import.
42 from _pytest.main import Session
43 from _pytest._code.code import _TracebackStyle
46SEP = "/"
48tracebackcutdir = Path(_pytest.__file__).parent
51def iterparentnodeids(nodeid: str) -> Iterator[str]:
52 """Return the parent node IDs of a given node ID, inclusive.
54 For the node ID
56 "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
58 the result would be
60 ""
61 "testing"
62 "testing/code"
63 "testing/code/test_excinfo.py"
64 "testing/code/test_excinfo.py::TestFormattedExcinfo"
65 "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
67 Note that / components are only considered until the first ::.
68 """
69 pos = 0
70 first_colons: Optional[int] = nodeid.find("::")
71 if first_colons == -1:
72 first_colons = None
73 # The root Session node - always present.
74 yield ""
75 # Eagerly consume SEP parts until first colons.
76 while True:
77 at = nodeid.find(SEP, pos, first_colons)
78 if at == -1:
79 break
80 if at > 0:
81 yield nodeid[:at]
82 pos = at + len(SEP)
83 # Eagerly consume :: parts.
84 while True:
85 at = nodeid.find("::", pos)
86 if at == -1:
87 break
88 if at > 0:
89 yield nodeid[:at]
90 pos = at + len("::")
91 # The node ID itself.
92 if nodeid:
93 yield nodeid
96def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
97 if Path(fspath) != path:
98 raise ValueError(
99 f"Path({fspath!r}) != {path!r}\n"
100 "if both path and fspath are given they need to be equal"
101 )
104def _imply_path(
105 node_type: Type["Node"],
106 path: Optional[Path],
107 fspath: Optional[LEGACY_PATH],
108) -> Path:
109 if fspath is not None:
110 warnings.warn(
111 NODE_CTOR_FSPATH_ARG.format(
112 node_type_name=node_type.__name__,
113 ),
114 stacklevel=6,
115 )
116 if path is not None:
117 if fspath is not None:
118 _check_path(path, fspath)
119 return path
120 else:
121 assert fspath is not None
122 return Path(fspath)
125_NodeType = TypeVar("_NodeType", bound="Node")
128class NodeMeta(type):
129 def __call__(self, *k, **kw):
130 msg = (
131 "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
132 "See "
133 "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
134 " for more details."
135 ).format(name=f"{self.__module__}.{self.__name__}")
136 fail(msg, pytrace=False)
138 def _create(self, *k, **kw):
139 try:
140 return super().__call__(*k, **kw)
141 except TypeError:
142 sig = signature(getattr(self, "__init__"))
143 known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
144 from .warning_types import PytestDeprecationWarning
146 warnings.warn(
147 PytestDeprecationWarning(
148 f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
149 "See https://docs.pytest.org/en/stable/deprecations.html"
150 "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
151 "for more details."
152 )
153 )
155 return super().__call__(*k, **known_kw)
158class Node(metaclass=NodeMeta):
159 """Base class for Collector and Item, the components of the test
160 collection tree.
162 Collector subclasses have children; Items are leaf nodes.
163 """
165 # Implemented in the legacypath plugin.
166 #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
167 #: for methods not migrated to ``pathlib.Path`` yet, such as
168 #: :meth:`Item.reportinfo`. Will be deprecated in a future release, prefer
169 #: using :attr:`path` instead.
170 fspath: LEGACY_PATH
172 # Use __slots__ to make attribute access faster.
173 # Note that __dict__ is still available.
174 __slots__ = (
175 "name",
176 "parent",
177 "config",
178 "session",
179 "path",
180 "_nodeid",
181 "_store",
182 "__dict__",
183 )
185 def __init__(
186 self,
187 name: str,
188 parent: "Optional[Node]" = None,
189 config: Optional[Config] = None,
190 session: "Optional[Session]" = None,
191 fspath: Optional[LEGACY_PATH] = None,
192 path: Optional[Path] = None,
193 nodeid: Optional[str] = None,
194 ) -> None:
195 #: A unique name within the scope of the parent node.
196 self.name: str = name
198 #: The parent collector node.
199 self.parent = parent
201 if config:
202 #: The pytest config object.
203 self.config: Config = config
204 else:
205 if not parent:
206 raise TypeError("config or parent must be provided")
207 self.config = parent.config
209 if session:
210 #: The pytest session this node is part of.
211 self.session: Session = session
212 else:
213 if not parent:
214 raise TypeError("session or parent must be provided")
215 self.session = parent.session
217 if path is None and fspath is None:
218 path = getattr(parent, "path", None)
219 #: Filesystem path where this node was collected from (can be None).
220 self.path: Path = _imply_path(type(self), path, fspath=fspath)
222 # The explicit annotation is to avoid publicly exposing NodeKeywords.
223 #: Keywords/markers collected from all scopes.
224 self.keywords: MutableMapping[str, Any] = NodeKeywords(self)
226 #: The marker objects belonging to this node.
227 self.own_markers: List[Mark] = []
229 #: Allow adding of extra keywords to use for matching.
230 self.extra_keyword_matches: Set[str] = set()
232 if nodeid is not None:
233 assert "::()" not in nodeid
234 self._nodeid = nodeid
235 else:
236 if not self.parent:
237 raise TypeError("nodeid or parent must be provided")
238 self._nodeid = self.parent.nodeid + "::" + self.name
240 #: A place where plugins can store information on the node for their
241 #: own use.
242 self.stash: Stash = Stash()
243 # Deprecated alias. Was never public. Can be removed in a few releases.
244 self._store = self.stash
246 @classmethod
247 def from_parent(cls, parent: "Node", **kw):
248 """Public constructor for Nodes.
250 This indirection got introduced in order to enable removing
251 the fragile logic from the node constructors.
253 Subclasses can use ``super().from_parent(...)`` when overriding the
254 construction.
256 :param parent: The parent node of this Node.
257 """
258 if "config" in kw:
259 raise TypeError("config is not a valid argument for from_parent")
260 if "session" in kw:
261 raise TypeError("session is not a valid argument for from_parent")
262 return cls._create(parent=parent, **kw)
264 @property
265 def ihook(self):
266 """fspath-sensitive hook proxy used to call pytest hooks."""
267 return self.session.gethookproxy(self.path)
269 def __repr__(self) -> str:
270 return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
272 def warn(self, warning: Warning) -> None:
273 """Issue a warning for this Node.
275 Warnings will be displayed after the test session, unless explicitly suppressed.
277 :param Warning warning:
278 The warning instance to issue.
280 :raises ValueError: If ``warning`` instance is not a subclass of Warning.
282 Example usage:
284 .. code-block:: python
286 node.warn(PytestWarning("some message"))
287 node.warn(UserWarning("some message"))
289 .. versionchanged:: 6.2
290 Any subclass of :class:`Warning` is now accepted, rather than only
291 :class:`PytestWarning <pytest.PytestWarning>` subclasses.
292 """
293 # enforce type checks here to avoid getting a generic type error later otherwise.
294 if not isinstance(warning, Warning):
295 raise ValueError(
296 "warning must be an instance of Warning or subclass, got {!r}".format(
297 warning
298 )
299 )
300 path, lineno = get_fslocation_from_item(self)
301 assert lineno is not None
302 warnings.warn_explicit(
303 warning,
304 category=None,
305 filename=str(path),
306 lineno=lineno + 1,
307 )
309 # Methods for ordering nodes.
311 @property
312 def nodeid(self) -> str:
313 """A ::-separated string denoting its collection tree address."""
314 return self._nodeid
316 def __hash__(self) -> int:
317 return hash(self._nodeid)
319 def setup(self) -> None:
320 pass
322 def teardown(self) -> None:
323 pass
325 def listchain(self) -> List["Node"]:
326 """Return list of all parent collectors up to self, starting from
327 the root of collection tree.
329 :returns: The nodes.
330 """
331 chain = []
332 item: Optional[Node] = self
333 while item is not None:
334 chain.append(item)
335 item = item.parent
336 chain.reverse()
337 return chain
339 def add_marker(
340 self, marker: Union[str, MarkDecorator], append: bool = True
341 ) -> None:
342 """Dynamically add a marker object to the node.
344 :param marker:
345 The marker.
346 :param append:
347 Whether to append the marker, or prepend it.
348 """
349 from _pytest.mark import MARK_GEN
351 if isinstance(marker, MarkDecorator):
352 marker_ = marker
353 elif isinstance(marker, str):
354 marker_ = getattr(MARK_GEN, marker)
355 else:
356 raise ValueError("is not a string or pytest.mark.* Marker")
357 self.keywords[marker_.name] = marker_
358 if append:
359 self.own_markers.append(marker_.mark)
360 else:
361 self.own_markers.insert(0, marker_.mark)
363 def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
364 """Iterate over all markers of the node.
366 :param name: If given, filter the results by the name attribute.
367 :returns: An iterator of the markers of the node.
368 """
369 return (x[1] for x in self.iter_markers_with_node(name=name))
371 def iter_markers_with_node(
372 self, name: Optional[str] = None
373 ) -> Iterator[Tuple["Node", Mark]]:
374 """Iterate over all markers of the node.
376 :param name: If given, filter the results by the name attribute.
377 :returns: An iterator of (node, mark) tuples.
378 """
379 for node in reversed(self.listchain()):
380 for mark in node.own_markers:
381 if name is None or getattr(mark, "name", None) == name:
382 yield node, mark
384 @overload
385 def get_closest_marker(self, name: str) -> Optional[Mark]:
386 ...
388 @overload
389 def get_closest_marker(self, name: str, default: Mark) -> Mark:
390 ...
392 def get_closest_marker(
393 self, name: str, default: Optional[Mark] = None
394 ) -> Optional[Mark]:
395 """Return the first marker matching the name, from closest (for
396 example function) to farther level (for example module level).
398 :param default: Fallback return value if no marker was found.
399 :param name: Name to filter by.
400 """
401 return next(self.iter_markers(name=name), default)
403 def listextrakeywords(self) -> Set[str]:
404 """Return a set of all extra keywords in self and any parents."""
405 extra_keywords: Set[str] = set()
406 for item in self.listchain():
407 extra_keywords.update(item.extra_keyword_matches)
408 return extra_keywords
410 def listnames(self) -> List[str]:
411 return [x.name for x in self.listchain()]
413 def addfinalizer(self, fin: Callable[[], object]) -> None:
414 """Register a function to be called without arguments when this node is
415 finalized.
417 This method can only be called when this node is active
418 in a setup chain, for example during self.setup().
419 """
420 self.session._setupstate.addfinalizer(fin, self)
422 def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
423 """Get the next parent node (including self) which is an instance of
424 the given class.
426 :param cls: The node class to search for.
427 :returns: The node, if found.
428 """
429 current: Optional[Node] = self
430 while current and not isinstance(current, cls):
431 current = current.parent
432 assert current is None or isinstance(current, cls)
433 return current
435 def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
436 pass
438 def _repr_failure_py(
439 self,
440 excinfo: ExceptionInfo[BaseException],
441 style: "Optional[_TracebackStyle]" = None,
442 ) -> TerminalRepr:
443 from _pytest.fixtures import FixtureLookupError
445 if isinstance(excinfo.value, ConftestImportFailure):
446 excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
447 if isinstance(excinfo.value, fail.Exception):
448 if not excinfo.value.pytrace:
449 style = "value"
450 if isinstance(excinfo.value, FixtureLookupError):
451 return excinfo.value.formatrepr()
452 if self.config.getoption("fulltrace", False):
453 style = "long"
454 else:
455 tb = _pytest._code.Traceback([excinfo.traceback[-1]])
456 self._prunetraceback(excinfo)
457 if len(excinfo.traceback) == 0:
458 excinfo.traceback = tb
459 if style == "auto":
460 style = "long"
461 # XXX should excinfo.getrepr record all data and toterminal() process it?
462 if style is None:
463 if self.config.getoption("tbstyle", "auto") == "short":
464 style = "short"
465 else:
466 style = "long"
468 if self.config.getoption("verbose", 0) > 1:
469 truncate_locals = False
470 else:
471 truncate_locals = True
473 # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
474 # It is possible for a fixture/test to change the CWD while this code runs, which
475 # would then result in the user seeing confusing paths in the failure message.
476 # To fix this, if the CWD changed, always display the full absolute path.
477 # It will be better to just always display paths relative to invocation_dir, but
478 # this requires a lot of plumbing (#6428).
479 try:
480 abspath = Path(os.getcwd()) != self.config.invocation_params.dir
481 except OSError:
482 abspath = True
484 return excinfo.getrepr(
485 funcargs=True,
486 abspath=abspath,
487 showlocals=self.config.getoption("showlocals", False),
488 style=style,
489 tbfilter=False, # pruned already, or in --fulltrace mode.
490 truncate_locals=truncate_locals,
491 )
493 def repr_failure(
494 self,
495 excinfo: ExceptionInfo[BaseException],
496 style: "Optional[_TracebackStyle]" = None,
497 ) -> Union[str, TerminalRepr]:
498 """Return a representation of a collection or test failure.
500 .. seealso:: :ref:`non-python tests`
502 :param excinfo: Exception information for the failure.
503 """
504 return self._repr_failure_py(excinfo, style)
507def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]:
508 """Try to extract the actual location from a node, depending on available attributes:
510 * "location": a pair (path, lineno)
511 * "obj": a Python object that the node wraps.
512 * "fspath": just a path
514 :rtype: A tuple of (str|Path, int) with filename and line number.
515 """
516 # See Item.location.
517 location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
518 if location is not None:
519 return location[:2]
520 obj = getattr(node, "obj", None)
521 if obj is not None:
522 return getfslineno(obj)
523 return getattr(node, "fspath", "unknown location"), -1
526class Collector(Node):
527 """Collector instances create children through collect() and thus
528 iteratively build a tree."""
530 class CollectError(Exception):
531 """An error during collection, contains a custom message."""
533 def collect(self) -> Iterable[Union["Item", "Collector"]]:
534 """Return a list of children (items and collectors) for this
535 collection node."""
536 raise NotImplementedError("abstract")
538 # TODO: This omits the style= parameter which breaks Liskov Substitution.
539 def repr_failure( # type: ignore[override]
540 self, excinfo: ExceptionInfo[BaseException]
541 ) -> Union[str, TerminalRepr]:
542 """Return a representation of a collection failure.
544 :param excinfo: Exception information for the failure.
545 """
546 if isinstance(excinfo.value, self.CollectError) and not self.config.getoption(
547 "fulltrace", False
548 ):
549 exc = excinfo.value
550 return str(exc.args[0])
552 # Respect explicit tbstyle option, but default to "short"
553 # (_repr_failure_py uses "long" with "fulltrace" option always).
554 tbstyle = self.config.getoption("tbstyle", "auto")
555 if tbstyle == "auto":
556 tbstyle = "short"
558 return self._repr_failure_py(excinfo, style=tbstyle)
560 def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
561 if hasattr(self, "path"):
562 traceback = excinfo.traceback
563 ntraceback = traceback.cut(path=self.path)
564 if ntraceback == traceback:
565 ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
566 excinfo.traceback = ntraceback.filter()
569def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
570 for initial_path in session._initialpaths:
571 if commonpath(path, initial_path) == initial_path:
572 rel = str(path.relative_to(initial_path))
573 return "" if rel == "." else rel
574 return None
577class FSCollector(Collector):
578 def __init__(
579 self,
580 fspath: Optional[LEGACY_PATH] = None,
581 path_or_parent: Optional[Union[Path, Node]] = None,
582 path: Optional[Path] = None,
583 name: Optional[str] = None,
584 parent: Optional[Node] = None,
585 config: Optional[Config] = None,
586 session: Optional["Session"] = None,
587 nodeid: Optional[str] = None,
588 ) -> None:
589 if path_or_parent:
590 if isinstance(path_or_parent, Node):
591 assert parent is None
592 parent = cast(FSCollector, path_or_parent)
593 elif isinstance(path_or_parent, Path):
594 assert path is None
595 path = path_or_parent
597 path = _imply_path(type(self), path, fspath=fspath)
598 if name is None:
599 name = path.name
600 if parent is not None and parent.path != path:
601 try:
602 rel = path.relative_to(parent.path)
603 except ValueError:
604 pass
605 else:
606 name = str(rel)
607 name = name.replace(os.sep, SEP)
608 self.path = path
610 if session is None:
611 assert parent is not None
612 session = parent.session
614 if nodeid is None:
615 try:
616 nodeid = str(self.path.relative_to(session.config.rootpath))
617 except ValueError:
618 nodeid = _check_initialpaths_for_relpath(session, path)
620 if nodeid and os.sep != SEP:
621 nodeid = nodeid.replace(os.sep, SEP)
623 super().__init__(
624 name=name,
625 parent=parent,
626 config=config,
627 session=session,
628 nodeid=nodeid,
629 path=path,
630 )
632 @classmethod
633 def from_parent(
634 cls,
635 parent,
636 *,
637 fspath: Optional[LEGACY_PATH] = None,
638 path: Optional[Path] = None,
639 **kw,
640 ):
641 """The public constructor."""
642 return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
644 def gethookproxy(self, fspath: "os.PathLike[str]"):
645 warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
646 return self.session.gethookproxy(fspath)
648 def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
649 warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
650 return self.session.isinitpath(path)
653class File(FSCollector):
654 """Base class for collecting tests from a file.
656 :ref:`non-python tests`.
657 """
660class Item(Node):
661 """A basic test invocation item.
663 Note that for a single function there might be multiple test invocation items.
664 """
666 nextitem = None
668 def __init__(
669 self,
670 name,
671 parent=None,
672 config: Optional[Config] = None,
673 session: Optional["Session"] = None,
674 nodeid: Optional[str] = None,
675 **kw,
676 ) -> None:
677 # The first two arguments are intentionally passed positionally,
678 # to keep plugins who define a node type which inherits from
679 # (pytest.Item, pytest.File) working (see issue #8435).
680 # They can be made kwargs when the deprecation above is done.
681 super().__init__(
682 name,
683 parent,
684 config=config,
685 session=session,
686 nodeid=nodeid,
687 **kw,
688 )
689 self._report_sections: List[Tuple[str, str, str]] = []
691 #: A list of tuples (name, value) that holds user defined properties
692 #: for this test.
693 self.user_properties: List[Tuple[str, object]] = []
695 self._check_item_and_collector_diamond_inheritance()
697 def _check_item_and_collector_diamond_inheritance(self) -> None:
698 """
699 Check if the current type inherits from both File and Collector
700 at the same time, emitting a warning accordingly (#8447).
701 """
702 cls = type(self)
704 # We inject an attribute in the type to avoid issuing this warning
705 # for the same class more than once, which is not helpful.
706 # It is a hack, but was deemed acceptable in order to avoid
707 # flooding the user in the common case.
708 attr_name = "_pytest_diamond_inheritance_warning_shown"
709 if getattr(cls, attr_name, False):
710 return
711 setattr(cls, attr_name, True)
713 problems = ", ".join(
714 base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
715 )
716 if problems:
717 warnings.warn(
718 f"{cls.__name__} is an Item subclass and should not be a collector, "
719 f"however its bases {problems} are collectors.\n"
720 "Please split the Collectors and the Item into separate node types.\n"
721 "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
722 "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
723 PytestWarning,
724 )
726 def runtest(self) -> None:
727 """Run the test case for this item.
729 Must be implemented by subclasses.
731 .. seealso:: :ref:`non-python tests`
732 """
733 raise NotImplementedError("runtest must be implemented by Item subclass")
735 def add_report_section(self, when: str, key: str, content: str) -> None:
736 """Add a new report section, similar to what's done internally to add
737 stdout and stderr captured output::
739 item.add_report_section("call", "stdout", "report section contents")
741 :param str when:
742 One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
743 :param str key:
744 Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
745 ``"stderr"`` internally.
746 :param str content:
747 The full contents as a string.
748 """
749 if content:
750 self._report_sections.append((when, key, content))
752 def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
753 """Get location information for this item for test reports.
755 Returns a tuple with three elements:
757 - The path of the test (default ``self.path``)
758 - The line number of the test (default ``None``)
759 - A name of the test to be shown (default ``""``)
761 .. seealso:: :ref:`non-python tests`
762 """
763 return self.path, None, ""
765 @cached_property
766 def location(self) -> Tuple[str, Optional[int], str]:
767 location = self.reportinfo()
768 path = absolutepath(os.fspath(location[0]))
769 relfspath = self.session._node_location_to_relpath(path)
770 assert type(location[2]) is str
771 return (relfspath, location[1], location[2])