nodes.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. # mypy: allow-untyped-defs
  2. from __future__ import annotations
  3. import abc
  4. from collections.abc import Callable
  5. from collections.abc import Iterable
  6. from collections.abc import Iterator
  7. from collections.abc import MutableMapping
  8. from functools import cached_property
  9. from functools import lru_cache
  10. import os
  11. import pathlib
  12. from pathlib import Path
  13. from typing import Any
  14. from typing import cast
  15. from typing import NoReturn
  16. from typing import overload
  17. from typing import TYPE_CHECKING
  18. from typing import TypeVar
  19. import warnings
  20. import pluggy
  21. import _pytest._code
  22. from _pytest._code import getfslineno
  23. from _pytest._code.code import ExceptionInfo
  24. from _pytest._code.code import TerminalRepr
  25. from _pytest._code.code import Traceback
  26. from _pytest._code.code import TracebackStyle
  27. from _pytest.compat import LEGACY_PATH
  28. from _pytest.compat import signature
  29. from _pytest.config import Config
  30. from _pytest.config import ConftestImportFailure
  31. from _pytest.config.compat import _check_path
  32. from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
  33. from _pytest.mark.structures import Mark
  34. from _pytest.mark.structures import MarkDecorator
  35. from _pytest.mark.structures import NodeKeywords
  36. from _pytest.outcomes import fail
  37. from _pytest.pathlib import absolutepath
  38. from _pytest.stash import Stash
  39. from _pytest.warning_types import PytestWarning
  40. if TYPE_CHECKING:
  41. from typing_extensions import Self
  42. # Imported here due to circular import.
  43. from _pytest.main import Session
  44. SEP = "/"
  45. tracebackcutdir = Path(_pytest.__file__).parent
  46. _T = TypeVar("_T")
  47. def _imply_path(
  48. node_type: type[Node],
  49. path: Path | None,
  50. fspath: LEGACY_PATH | None,
  51. ) -> Path:
  52. if fspath is not None:
  53. warnings.warn(
  54. NODE_CTOR_FSPATH_ARG.format(
  55. node_type_name=node_type.__name__,
  56. ),
  57. stacklevel=6,
  58. )
  59. if path is not None:
  60. if fspath is not None:
  61. _check_path(path, fspath)
  62. return path
  63. else:
  64. assert fspath is not None
  65. return Path(fspath)
  66. _NodeType = TypeVar("_NodeType", bound="Node")
  67. class NodeMeta(abc.ABCMeta):
  68. """Metaclass used by :class:`Node` to enforce that direct construction raises
  69. :class:`Failed`.
  70. This behaviour supports the indirection introduced with :meth:`Node.from_parent`,
  71. the named constructor to be used instead of direct construction. The design
  72. decision to enforce indirection with :class:`NodeMeta` was made as a
  73. temporary aid for refactoring the collection tree, which was diagnosed to
  74. have :class:`Node` objects whose creational patterns were overly entangled.
  75. Once the refactoring is complete, this metaclass can be removed.
  76. See https://github.com/pytest-dev/pytest/projects/3 for an overview of the
  77. progress on detangling the :class:`Node` classes.
  78. """
  79. def __call__(cls, *k, **kw) -> NoReturn:
  80. msg = (
  81. "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
  82. "See "
  83. "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
  84. " for more details."
  85. ).format(name=f"{cls.__module__}.{cls.__name__}")
  86. fail(msg, pytrace=False)
  87. def _create(cls: type[_T], *k, **kw) -> _T:
  88. try:
  89. return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
  90. except TypeError:
  91. sig = signature(getattr(cls, "__init__"))
  92. known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
  93. from .warning_types import PytestDeprecationWarning
  94. warnings.warn(
  95. PytestDeprecationWarning(
  96. f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
  97. "See https://docs.pytest.org/en/stable/deprecations.html"
  98. "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
  99. "for more details."
  100. )
  101. )
  102. return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc]
  103. class Node(abc.ABC, metaclass=NodeMeta):
  104. r"""Base class of :class:`Collector` and :class:`Item`, the components of
  105. the test collection tree.
  106. ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
  107. leaf nodes.
  108. """
  109. # Implemented in the legacypath plugin.
  110. #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
  111. #: for methods not migrated to ``pathlib.Path`` yet, such as
  112. #: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
  113. #: a future release, prefer using :attr:`path` instead.
  114. fspath: LEGACY_PATH
  115. # Use __slots__ to make attribute access faster.
  116. # Note that __dict__ is still available.
  117. __slots__ = (
  118. "__dict__",
  119. "_nodeid",
  120. "_store",
  121. "config",
  122. "name",
  123. "parent",
  124. "path",
  125. "session",
  126. )
  127. def __init__(
  128. self,
  129. name: str,
  130. parent: Node | None = None,
  131. config: Config | None = None,
  132. session: Session | None = None,
  133. fspath: LEGACY_PATH | None = None,
  134. path: Path | None = None,
  135. nodeid: str | None = None,
  136. ) -> None:
  137. #: A unique name within the scope of the parent node.
  138. self.name: str = name
  139. #: The parent collector node.
  140. self.parent = parent
  141. if config:
  142. #: The pytest config object.
  143. self.config: Config = config
  144. else:
  145. if not parent:
  146. raise TypeError("config or parent must be provided")
  147. self.config = parent.config
  148. if session:
  149. #: The pytest session this node is part of.
  150. self.session: Session = session
  151. else:
  152. if not parent:
  153. raise TypeError("session or parent must be provided")
  154. self.session = parent.session
  155. if path is None and fspath is None:
  156. path = getattr(parent, "path", None)
  157. #: Filesystem path where this node was collected from (can be None).
  158. self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
  159. # The explicit annotation is to avoid publicly exposing NodeKeywords.
  160. #: Keywords/markers collected from all scopes.
  161. self.keywords: MutableMapping[str, Any] = NodeKeywords(self)
  162. #: The marker objects belonging to this node.
  163. self.own_markers: list[Mark] = []
  164. #: Allow adding of extra keywords to use for matching.
  165. self.extra_keyword_matches: set[str] = set()
  166. if nodeid is not None:
  167. assert "::()" not in nodeid
  168. self._nodeid = nodeid
  169. else:
  170. if not self.parent:
  171. raise TypeError("nodeid or parent must be provided")
  172. self._nodeid = self.parent.nodeid + "::" + self.name
  173. #: A place where plugins can store information on the node for their
  174. #: own use.
  175. self.stash: Stash = Stash()
  176. # Deprecated alias. Was never public. Can be removed in a few releases.
  177. self._store = self.stash
  178. @classmethod
  179. def from_parent(cls, parent: Node, **kw) -> Self:
  180. """Public constructor for Nodes.
  181. This indirection got introduced in order to enable removing
  182. the fragile logic from the node constructors.
  183. Subclasses can use ``super().from_parent(...)`` when overriding the
  184. construction.
  185. :param parent: The parent node of this Node.
  186. """
  187. if "config" in kw:
  188. raise TypeError("config is not a valid argument for from_parent")
  189. if "session" in kw:
  190. raise TypeError("session is not a valid argument for from_parent")
  191. return cls._create(parent=parent, **kw)
  192. @property
  193. def ihook(self) -> pluggy.HookRelay:
  194. """fspath-sensitive hook proxy used to call pytest hooks."""
  195. return self.session.gethookproxy(self.path)
  196. def __repr__(self) -> str:
  197. return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
  198. def warn(self, warning: Warning) -> None:
  199. """Issue a warning for this Node.
  200. Warnings will be displayed after the test session, unless explicitly suppressed.
  201. :param Warning warning:
  202. The warning instance to issue.
  203. :raises ValueError: If ``warning`` instance is not a subclass of Warning.
  204. Example usage:
  205. .. code-block:: python
  206. node.warn(PytestWarning("some message"))
  207. node.warn(UserWarning("some message"))
  208. .. versionchanged:: 6.2
  209. Any subclass of :class:`Warning` is now accepted, rather than only
  210. :class:`PytestWarning <pytest.PytestWarning>` subclasses.
  211. """
  212. # enforce type checks here to avoid getting a generic type error later otherwise.
  213. if not isinstance(warning, Warning):
  214. raise ValueError(
  215. f"warning must be an instance of Warning or subclass, got {warning!r}"
  216. )
  217. path, lineno = get_fslocation_from_item(self)
  218. assert lineno is not None
  219. warnings.warn_explicit(
  220. warning,
  221. category=None,
  222. filename=str(path),
  223. lineno=lineno + 1,
  224. )
  225. # Methods for ordering nodes.
  226. @property
  227. def nodeid(self) -> str:
  228. """A ::-separated string denoting its collection tree address."""
  229. return self._nodeid
  230. def __hash__(self) -> int:
  231. return hash(self._nodeid)
  232. def setup(self) -> None:
  233. pass
  234. def teardown(self) -> None:
  235. pass
  236. def iter_parents(self) -> Iterator[Node]:
  237. """Iterate over all parent collectors starting from and including self
  238. up to the root of the collection tree.
  239. .. versionadded:: 8.1
  240. """
  241. parent: Node | None = self
  242. while parent is not None:
  243. yield parent
  244. parent = parent.parent
  245. def listchain(self) -> list[Node]:
  246. """Return a list of all parent collectors starting from the root of the
  247. collection tree down to and including self."""
  248. chain = []
  249. item: Node | None = self
  250. while item is not None:
  251. chain.append(item)
  252. item = item.parent
  253. chain.reverse()
  254. return chain
  255. def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None:
  256. """Dynamically add a marker object to the node.
  257. :param marker:
  258. The marker.
  259. :param append:
  260. Whether to append the marker, or prepend it.
  261. """
  262. from _pytest.mark import MARK_GEN
  263. if isinstance(marker, MarkDecorator):
  264. marker_ = marker
  265. elif isinstance(marker, str):
  266. marker_ = getattr(MARK_GEN, marker)
  267. else:
  268. raise ValueError("is not a string or pytest.mark.* Marker")
  269. self.keywords[marker_.name] = marker_
  270. if append:
  271. self.own_markers.append(marker_.mark)
  272. else:
  273. self.own_markers.insert(0, marker_.mark)
  274. def iter_markers(self, name: str | None = None) -> Iterator[Mark]:
  275. """Iterate over all markers of the node.
  276. :param name: If given, filter the results by the name attribute.
  277. :returns: An iterator of the markers of the node.
  278. """
  279. return (x[1] for x in self.iter_markers_with_node(name=name))
  280. def iter_markers_with_node(
  281. self, name: str | None = None
  282. ) -> Iterator[tuple[Node, Mark]]:
  283. """Iterate over all markers of the node.
  284. :param name: If given, filter the results by the name attribute.
  285. :returns: An iterator of (node, mark) tuples.
  286. """
  287. for node in self.iter_parents():
  288. for mark in node.own_markers:
  289. if name is None or getattr(mark, "name", None) == name:
  290. yield node, mark
  291. @overload
  292. def get_closest_marker(self, name: str) -> Mark | None: ...
  293. @overload
  294. def get_closest_marker(self, name: str, default: Mark) -> Mark: ...
  295. def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None:
  296. """Return the first marker matching the name, from closest (for
  297. example function) to farther level (for example module level).
  298. :param default: Fallback return value if no marker was found.
  299. :param name: Name to filter by.
  300. """
  301. return next(self.iter_markers(name=name), default)
  302. def listextrakeywords(self) -> set[str]:
  303. """Return a set of all extra keywords in self and any parents."""
  304. extra_keywords: set[str] = set()
  305. for item in self.listchain():
  306. extra_keywords.update(item.extra_keyword_matches)
  307. return extra_keywords
  308. def listnames(self) -> list[str]:
  309. return [x.name for x in self.listchain()]
  310. def addfinalizer(self, fin: Callable[[], object]) -> None:
  311. """Register a function to be called without arguments when this node is
  312. finalized.
  313. This method can only be called when this node is active
  314. in a setup chain, for example during self.setup().
  315. """
  316. self.session._setupstate.addfinalizer(fin, self)
  317. def getparent(self, cls: type[_NodeType]) -> _NodeType | None:
  318. """Get the closest parent node (including self) which is an instance of
  319. the given class.
  320. :param cls: The node class to search for.
  321. :returns: The node, if found.
  322. """
  323. for node in self.iter_parents():
  324. if isinstance(node, cls):
  325. return node
  326. return None
  327. def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
  328. return excinfo.traceback
  329. def _repr_failure_py(
  330. self,
  331. excinfo: ExceptionInfo[BaseException],
  332. style: TracebackStyle | None = None,
  333. ) -> TerminalRepr:
  334. from _pytest.fixtures import FixtureLookupError
  335. if isinstance(excinfo.value, ConftestImportFailure):
  336. excinfo = ExceptionInfo.from_exception(excinfo.value.cause)
  337. if isinstance(excinfo.value, fail.Exception):
  338. if not excinfo.value.pytrace:
  339. style = "value"
  340. if isinstance(excinfo.value, FixtureLookupError):
  341. return excinfo.value.formatrepr()
  342. tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback]
  343. if self.config.getoption("fulltrace", False):
  344. style = "long"
  345. tbfilter = False
  346. else:
  347. tbfilter = self._traceback_filter
  348. if style == "auto":
  349. style = "long"
  350. # XXX should excinfo.getrepr record all data and toterminal() process it?
  351. if style is None:
  352. if self.config.getoption("tbstyle", "auto") == "short":
  353. style = "short"
  354. else:
  355. style = "long"
  356. if self.config.get_verbosity() > 1:
  357. truncate_locals = False
  358. else:
  359. truncate_locals = True
  360. truncate_args = False if self.config.get_verbosity() > 2 else True
  361. # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
  362. # It is possible for a fixture/test to change the CWD while this code runs, which
  363. # would then result in the user seeing confusing paths in the failure message.
  364. # To fix this, if the CWD changed, always display the full absolute path.
  365. # It will be better to just always display paths relative to invocation_dir, but
  366. # this requires a lot of plumbing (#6428).
  367. try:
  368. abspath = Path(os.getcwd()) != self.config.invocation_params.dir
  369. except OSError:
  370. abspath = True
  371. return excinfo.getrepr(
  372. funcargs=True,
  373. abspath=abspath,
  374. showlocals=self.config.getoption("showlocals", False),
  375. style=style,
  376. tbfilter=tbfilter,
  377. truncate_locals=truncate_locals,
  378. truncate_args=truncate_args,
  379. )
  380. def repr_failure(
  381. self,
  382. excinfo: ExceptionInfo[BaseException],
  383. style: TracebackStyle | None = None,
  384. ) -> str | TerminalRepr:
  385. """Return a representation of a collection or test failure.
  386. .. seealso:: :ref:`non-python tests`
  387. :param excinfo: Exception information for the failure.
  388. """
  389. return self._repr_failure_py(excinfo, style)
  390. def get_fslocation_from_item(node: Node) -> tuple[str | Path, int | None]:
  391. """Try to extract the actual location from a node, depending on available attributes:
  392. * "location": a pair (path, lineno)
  393. * "obj": a Python object that the node wraps.
  394. * "path": just a path
  395. :rtype: A tuple of (str|Path, int) with filename and 0-based line number.
  396. """
  397. # See Item.location.
  398. location: tuple[str, int | None, str] | None = getattr(node, "location", None)
  399. if location is not None:
  400. return location[:2]
  401. obj = getattr(node, "obj", None)
  402. if obj is not None:
  403. return getfslineno(obj)
  404. return getattr(node, "path", "unknown location"), -1
  405. class Collector(Node, abc.ABC):
  406. """Base class of all collectors.
  407. Collector create children through `collect()` and thus iteratively build
  408. the collection tree.
  409. """
  410. class CollectError(Exception):
  411. """An error during collection, contains a custom message."""
  412. @abc.abstractmethod
  413. def collect(self) -> Iterable[Item | Collector]:
  414. """Collect children (items and collectors) for this collector."""
  415. raise NotImplementedError("abstract")
  416. # TODO: This omits the style= parameter which breaks Liskov Substitution.
  417. def repr_failure( # type: ignore[override]
  418. self, excinfo: ExceptionInfo[BaseException]
  419. ) -> str | TerminalRepr:
  420. """Return a representation of a collection failure.
  421. :param excinfo: Exception information for the failure.
  422. """
  423. if isinstance(excinfo.value, self.CollectError) and not self.config.getoption(
  424. "fulltrace", False
  425. ):
  426. exc = excinfo.value
  427. return str(exc.args[0])
  428. # Respect explicit tbstyle option, but default to "short"
  429. # (_repr_failure_py uses "long" with "fulltrace" option always).
  430. tbstyle = self.config.getoption("tbstyle", "auto")
  431. if tbstyle == "auto":
  432. tbstyle = "short"
  433. return self._repr_failure_py(excinfo, style=tbstyle)
  434. def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
  435. if hasattr(self, "path"):
  436. traceback = excinfo.traceback
  437. ntraceback = traceback.cut(path=self.path)
  438. if ntraceback == traceback:
  439. ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
  440. return ntraceback.filter(excinfo)
  441. return excinfo.traceback
  442. @lru_cache(maxsize=1000)
  443. def _check_initialpaths_for_relpath(
  444. initial_paths: frozenset[Path], path: Path
  445. ) -> str | None:
  446. if path in initial_paths:
  447. return ""
  448. for parent in path.parents:
  449. if parent in initial_paths:
  450. return str(path.relative_to(parent))
  451. return None
  452. class FSCollector(Collector, abc.ABC):
  453. """Base class for filesystem collectors."""
  454. def __init__(
  455. self,
  456. fspath: LEGACY_PATH | None = None,
  457. path_or_parent: Path | Node | None = None,
  458. path: Path | None = None,
  459. name: str | None = None,
  460. parent: Node | None = None,
  461. config: Config | None = None,
  462. session: Session | None = None,
  463. nodeid: str | None = None,
  464. ) -> None:
  465. if path_or_parent:
  466. if isinstance(path_or_parent, Node):
  467. assert parent is None
  468. parent = cast(FSCollector, path_or_parent)
  469. elif isinstance(path_or_parent, Path):
  470. assert path is None
  471. path = path_or_parent
  472. path = _imply_path(type(self), path, fspath=fspath)
  473. if name is None:
  474. name = path.name
  475. if parent is not None and parent.path != path:
  476. try:
  477. rel = path.relative_to(parent.path)
  478. except ValueError:
  479. pass
  480. else:
  481. name = str(rel)
  482. name = name.replace(os.sep, SEP)
  483. self.path = path
  484. if session is None:
  485. assert parent is not None
  486. session = parent.session
  487. if nodeid is None:
  488. try:
  489. nodeid = str(self.path.relative_to(session.config.rootpath))
  490. except ValueError:
  491. nodeid = _check_initialpaths_for_relpath(session._initialpaths, path)
  492. if nodeid and os.sep != SEP:
  493. nodeid = nodeid.replace(os.sep, SEP)
  494. super().__init__(
  495. name=name,
  496. parent=parent,
  497. config=config,
  498. session=session,
  499. nodeid=nodeid,
  500. path=path,
  501. )
  502. @classmethod
  503. def from_parent(
  504. cls,
  505. parent,
  506. *,
  507. fspath: LEGACY_PATH | None = None,
  508. path: Path | None = None,
  509. **kw,
  510. ) -> Self:
  511. """The public constructor."""
  512. return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
  513. class File(FSCollector, abc.ABC):
  514. """Base class for collecting tests from a file.
  515. :ref:`non-python tests`.
  516. """
  517. class Directory(FSCollector, abc.ABC):
  518. """Base class for collecting files from a directory.
  519. A basic directory collector does the following: goes over the files and
  520. sub-directories in the directory and creates collectors for them by calling
  521. the hooks :hook:`pytest_collect_directory` and :hook:`pytest_collect_file`,
  522. after checking that they are not ignored using
  523. :hook:`pytest_ignore_collect`.
  524. The default directory collectors are :class:`~pytest.Dir` and
  525. :class:`~pytest.Package`.
  526. .. versionadded:: 8.0
  527. :ref:`custom directory collectors`.
  528. """
  529. class Item(Node, abc.ABC):
  530. """Base class of all test invocation items.
  531. Note that for a single function there might be multiple test invocation items.
  532. """
  533. nextitem = None
  534. def __init__(
  535. self,
  536. name,
  537. parent=None,
  538. config: Config | None = None,
  539. session: Session | None = None,
  540. nodeid: str | None = None,
  541. **kw,
  542. ) -> None:
  543. # The first two arguments are intentionally passed positionally,
  544. # to keep plugins who define a node type which inherits from
  545. # (pytest.Item, pytest.File) working (see issue #8435).
  546. # They can be made kwargs when the deprecation above is done.
  547. super().__init__(
  548. name,
  549. parent,
  550. config=config,
  551. session=session,
  552. nodeid=nodeid,
  553. **kw,
  554. )
  555. self._report_sections: list[tuple[str, str, str]] = []
  556. #: A list of tuples (name, value) that holds user defined properties
  557. #: for this test.
  558. self.user_properties: list[tuple[str, object]] = []
  559. self._check_item_and_collector_diamond_inheritance()
  560. def _check_item_and_collector_diamond_inheritance(self) -> None:
  561. """
  562. Check if the current type inherits from both File and Collector
  563. at the same time, emitting a warning accordingly (#8447).
  564. """
  565. cls = type(self)
  566. # We inject an attribute in the type to avoid issuing this warning
  567. # for the same class more than once, which is not helpful.
  568. # It is a hack, but was deemed acceptable in order to avoid
  569. # flooding the user in the common case.
  570. attr_name = "_pytest_diamond_inheritance_warning_shown"
  571. if getattr(cls, attr_name, False):
  572. return
  573. setattr(cls, attr_name, True)
  574. problems = ", ".join(
  575. base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
  576. )
  577. if problems:
  578. warnings.warn(
  579. f"{cls.__name__} is an Item subclass and should not be a collector, "
  580. f"however its bases {problems} are collectors.\n"
  581. "Please split the Collectors and the Item into separate node types.\n"
  582. "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
  583. "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
  584. PytestWarning,
  585. )
  586. @abc.abstractmethod
  587. def runtest(self) -> None:
  588. """Run the test case for this item.
  589. Must be implemented by subclasses.
  590. .. seealso:: :ref:`non-python tests`
  591. """
  592. raise NotImplementedError("runtest must be implemented by Item subclass")
  593. def add_report_section(self, when: str, key: str, content: str) -> None:
  594. """Add a new report section, similar to what's done internally to add
  595. stdout and stderr captured output::
  596. item.add_report_section("call", "stdout", "report section contents")
  597. :param str when:
  598. One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
  599. :param str key:
  600. Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
  601. ``"stderr"`` internally.
  602. :param str content:
  603. The full contents as a string.
  604. """
  605. if content:
  606. self._report_sections.append((when, key, content))
  607. def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
  608. """Get location information for this item for test reports.
  609. Returns a tuple with three elements:
  610. - The path of the test (default ``self.path``)
  611. - The 0-based line number of the test (default ``None``)
  612. - A name of the test to be shown (default ``""``)
  613. .. seealso:: :ref:`non-python tests`
  614. """
  615. return self.path, None, ""
  616. @cached_property
  617. def location(self) -> tuple[str, int | None, str]:
  618. """
  619. Returns a tuple of ``(relfspath, lineno, testname)`` for this item
  620. where ``relfspath`` is file path relative to ``config.rootpath``
  621. and lineno is a 0-based line number.
  622. """
  623. location = self.reportinfo()
  624. path = absolutepath(location[0])
  625. relfspath = self.session._node_location_to_relpath(path)
  626. assert type(location[2]) is str
  627. return (relfspath, location[1], location[2])