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

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 

20 

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 

39 

40if TYPE_CHECKING: 

41 # Imported here due to circular import. 

42 from _pytest.main import Session 

43 from _pytest._code.code import _TracebackStyle 

44 

45 

46SEP = "/" 

47 

48tracebackcutdir = Path(_pytest.__file__).parent 

49 

50 

51def iterparentnodeids(nodeid: str) -> Iterator[str]: 

52 """Return the parent node IDs of a given node ID, inclusive. 

53 

54 For the node ID 

55 

56 "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" 

57 

58 the result would be 

59 

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" 

66 

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 

94 

95 

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 ) 

102 

103 

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) 

123 

124 

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

126 

127 

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) 

137 

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 

145 

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 ) 

154 

155 return super().__call__(*k, **known_kw) 

156 

157 

158class Node(metaclass=NodeMeta): 

159 """Base class for Collector and Item, the components of the test 

160 collection tree. 

161 

162 Collector subclasses have children; Items are leaf nodes. 

163 """ 

164 

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 

171 

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 ) 

184 

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 

197 

198 #: The parent collector node. 

199 self.parent = parent 

200 

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 

208 

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 

216 

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) 

221 

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) 

225 

226 #: The marker objects belonging to this node. 

227 self.own_markers: List[Mark] = [] 

228 

229 #: Allow adding of extra keywords to use for matching. 

230 self.extra_keyword_matches: Set[str] = set() 

231 

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 

239 

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 

245 

246 @classmethod 

247 def from_parent(cls, parent: "Node", **kw): 

248 """Public constructor for Nodes. 

249 

250 This indirection got introduced in order to enable removing 

251 the fragile logic from the node constructors. 

252 

253 Subclasses can use ``super().from_parent(...)`` when overriding the 

254 construction. 

255 

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) 

263 

264 @property 

265 def ihook(self): 

266 """fspath-sensitive hook proxy used to call pytest hooks.""" 

267 return self.session.gethookproxy(self.path) 

268 

269 def __repr__(self) -> str: 

270 return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) 

271 

272 def warn(self, warning: Warning) -> None: 

273 """Issue a warning for this Node. 

274 

275 Warnings will be displayed after the test session, unless explicitly suppressed. 

276 

277 :param Warning warning: 

278 The warning instance to issue. 

279 

280 :raises ValueError: If ``warning`` instance is not a subclass of Warning. 

281 

282 Example usage: 

283 

284 .. code-block:: python 

285 

286 node.warn(PytestWarning("some message")) 

287 node.warn(UserWarning("some message")) 

288 

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 ) 

308 

309 # Methods for ordering nodes. 

310 

311 @property 

312 def nodeid(self) -> str: 

313 """A ::-separated string denoting its collection tree address.""" 

314 return self._nodeid 

315 

316 def __hash__(self) -> int: 

317 return hash(self._nodeid) 

318 

319 def setup(self) -> None: 

320 pass 

321 

322 def teardown(self) -> None: 

323 pass 

324 

325 def listchain(self) -> List["Node"]: 

326 """Return list of all parent collectors up to self, starting from 

327 the root of collection tree. 

328 

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 

338 

339 def add_marker( 

340 self, marker: Union[str, MarkDecorator], append: bool = True 

341 ) -> None: 

342 """Dynamically add a marker object to the node. 

343 

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 

350 

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) 

362 

363 def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]: 

364 """Iterate over all markers of the node. 

365 

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)) 

370 

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. 

375 

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 

383 

384 @overload 

385 def get_closest_marker(self, name: str) -> Optional[Mark]: 

386 ... 

387 

388 @overload 

389 def get_closest_marker(self, name: str, default: Mark) -> Mark: 

390 ... 

391 

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). 

397 

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) 

402 

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 

409 

410 def listnames(self) -> List[str]: 

411 return [x.name for x in self.listchain()] 

412 

413 def addfinalizer(self, fin: Callable[[], object]) -> None: 

414 """Register a function to be called without arguments when this node is 

415 finalized. 

416 

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) 

421 

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. 

425 

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 

434 

435 def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: 

436 pass 

437 

438 def _repr_failure_py( 

439 self, 

440 excinfo: ExceptionInfo[BaseException], 

441 style: "Optional[_TracebackStyle]" = None, 

442 ) -> TerminalRepr: 

443 from _pytest.fixtures import FixtureLookupError 

444 

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" 

467 

468 if self.config.getoption("verbose", 0) > 1: 

469 truncate_locals = False 

470 else: 

471 truncate_locals = True 

472 

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 

483 

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 ) 

492 

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. 

499 

500 .. seealso:: :ref:`non-python tests` 

501 

502 :param excinfo: Exception information for the failure. 

503 """ 

504 return self._repr_failure_py(excinfo, style) 

505 

506 

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: 

509 

510 * "location": a pair (path, lineno) 

511 * "obj": a Python object that the node wraps. 

512 * "fspath": just a path 

513 

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 

524 

525 

526class Collector(Node): 

527 """Collector instances create children through collect() and thus 

528 iteratively build a tree.""" 

529 

530 class CollectError(Exception): 

531 """An error during collection, contains a custom message.""" 

532 

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") 

537 

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. 

543 

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]) 

551 

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" 

557 

558 return self._repr_failure_py(excinfo, style=tbstyle) 

559 

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() 

567 

568 

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 

575 

576 

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 

596 

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 

609 

610 if session is None: 

611 assert parent is not None 

612 session = parent.session 

613 

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) 

619 

620 if nodeid and os.sep != SEP: 

621 nodeid = nodeid.replace(os.sep, SEP) 

622 

623 super().__init__( 

624 name=name, 

625 parent=parent, 

626 config=config, 

627 session=session, 

628 nodeid=nodeid, 

629 path=path, 

630 ) 

631 

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) 

643 

644 def gethookproxy(self, fspath: "os.PathLike[str]"): 

645 warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) 

646 return self.session.gethookproxy(fspath) 

647 

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) 

651 

652 

653class File(FSCollector): 

654 """Base class for collecting tests from a file. 

655 

656 :ref:`non-python tests`. 

657 """ 

658 

659 

660class Item(Node): 

661 """A basic test invocation item. 

662 

663 Note that for a single function there might be multiple test invocation items. 

664 """ 

665 

666 nextitem = None 

667 

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]] = [] 

690 

691 #: A list of tuples (name, value) that holds user defined properties 

692 #: for this test. 

693 self.user_properties: List[Tuple[str, object]] = [] 

694 

695 self._check_item_and_collector_diamond_inheritance() 

696 

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) 

703 

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) 

712 

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 ) 

725 

726 def runtest(self) -> None: 

727 """Run the test case for this item. 

728 

729 Must be implemented by subclasses. 

730 

731 .. seealso:: :ref:`non-python tests` 

732 """ 

733 raise NotImplementedError("runtest must be implemented by Item subclass") 

734 

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:: 

738 

739 item.add_report_section("call", "stdout", "report section contents") 

740 

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)) 

751 

752 def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: 

753 """Get location information for this item for test reports. 

754 

755 Returns a tuple with three elements: 

756 

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 ``""``) 

760 

761 .. seealso:: :ref:`non-python tests` 

762 """ 

763 return self.path, None, "" 

764 

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])