Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import os 

2import warnings 

3from functools import lru_cache 

4from typing import Callable 

5from typing import Dict 

6from typing import Iterable 

7from typing import Iterator 

8from typing import List 

9from typing import Optional 

10from typing import Sequence 

11from typing import Set 

12from typing import Tuple 

13from typing import TypeVar 

14from typing import Union 

15 

16import py 

17 

18import _pytest._code 

19from _pytest._code import getfslineno 

20from _pytest._code.code import ExceptionInfo 

21from _pytest._code.code import TerminalRepr 

22from _pytest.compat import cached_property 

23from _pytest.compat import overload 

24from _pytest.compat import TYPE_CHECKING 

25from _pytest.config import Config 

26from _pytest.config import ConftestImportFailure 

27from _pytest.config import PytestPluginManager 

28from _pytest.deprecated import NODE_USE_FROM_PARENT 

29from _pytest.fixtures import FixtureDef 

30from _pytest.fixtures import FixtureLookupError 

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 Path 

36from _pytest.store import Store 

37 

38if TYPE_CHECKING: 

39 from typing import Type 

40 

41 # Imported here due to circular import. 

42 from _pytest.main import Session 

43 from _pytest.warning_types import PytestWarning 

44 from _pytest._code.code import _TracebackStyle 

45 

46 

47SEP = "/" 

48 

49tracebackcutdir = py.path.local(_pytest.__file__).dirpath() 

50 

51 

52@lru_cache(maxsize=None) 

53def _splitnode(nodeid: str) -> Tuple[str, ...]: 

54 """Split a nodeid into constituent 'parts'. 

55 

56 Node IDs are strings, and can be things like: 

57 '' 

58 'testing/code' 

59 'testing/code/test_excinfo.py' 

60 'testing/code/test_excinfo.py::TestFormattedExcinfo' 

61 

62 Return values are lists e.g. 

63 [] 

64 ['testing', 'code'] 

65 ['testing', 'code', 'test_excinfo.py'] 

66 ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo'] 

67 """ 

68 if nodeid == "": 

69 # If there is no root node at all, return an empty list so the caller's logic can remain sane 

70 return () 

71 parts = nodeid.split(SEP) 

72 # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar' 

73 parts[-1:] = parts[-1].split("::") 

74 # Convert parts into a tuple to avoid possible errors with caching of a mutable type 

75 return tuple(parts) 

76 

77 

78def ischildnode(baseid: str, nodeid: str) -> bool: 

79 """Return True if the nodeid is a child node of the baseid. 

80 

81 E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' 

82 """ 

83 base_parts = _splitnode(baseid) 

84 node_parts = _splitnode(nodeid) 

85 if len(node_parts) < len(base_parts): 

86 return False 

87 return node_parts[: len(base_parts)] == base_parts 

88 

89 

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

91 

92 

93class NodeMeta(type): 

94 def __call__(self, *k, **kw): 

95 warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2) 

96 return super().__call__(*k, **kw) 

97 

98 def _create(self, *k, **kw): 

99 return super().__call__(*k, **kw) 

100 

101 

102class Node(metaclass=NodeMeta): 

103 """ base class for Collector and Item the test collection tree. 

104 Collector subclasses have children, Items are terminal nodes.""" 

105 

106 # Use __slots__ to make attribute access faster. 

107 # Note that __dict__ is still available. 

108 __slots__ = ( 

109 "name", 

110 "parent", 

111 "config", 

112 "session", 

113 "fspath", 

114 "_nodeid", 

115 "_store", 

116 "__dict__", 

117 ) 

118 

119 def __init__( 

120 self, 

121 name: str, 

122 parent: "Optional[Node]" = None, 

123 config: Optional[Config] = None, 

124 session: "Optional[Session]" = None, 

125 fspath: Optional[py.path.local] = None, 

126 nodeid: Optional[str] = None, 

127 ) -> None: 

128 #: a unique name within the scope of the parent node 

129 self.name = name 

130 

131 #: the parent collector node. 

132 self.parent = parent 

133 

134 #: the pytest config object 

135 if config: 

136 self.config = config # type: Config 

137 else: 

138 if not parent: 

139 raise TypeError("config or parent must be provided") 

140 self.config = parent.config 

141 

142 #: the session this node is part of 

143 if session: 

144 self.session = session 

145 else: 

146 if not parent: 

147 raise TypeError("session or parent must be provided") 

148 self.session = parent.session 

149 

150 #: filesystem path where this node was collected from (can be None) 

151 self.fspath = fspath or getattr(parent, "fspath", None) 

152 

153 #: keywords/markers collected from all scopes 

154 self.keywords = NodeKeywords(self) 

155 

156 #: the marker objects belonging to this node 

157 self.own_markers = [] # type: List[Mark] 

158 

159 #: allow adding of extra keywords to use for matching 

160 self.extra_keyword_matches = set() # type: Set[str] 

161 

162 # used for storing artificial fixturedefs for direct parametrization 

163 self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef] 

164 

165 if nodeid is not None: 

166 assert "::()" not in nodeid 

167 self._nodeid = nodeid 

168 else: 

169 if not self.parent: 

170 raise TypeError("nodeid or parent must be provided") 

171 self._nodeid = self.parent.nodeid 

172 if self.name != "()": 

173 self._nodeid += "::" + self.name 

174 

175 # A place where plugins can store information on the node for their 

176 # own use. Currently only intended for internal plugins. 

177 self._store = Store() 

178 

179 @classmethod 

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

181 """ 

182 Public Constructor for Nodes 

183 

184 This indirection got introduced in order to enable removing 

185 the fragile logic from the node constructors. 

186 

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

188 

189 :param parent: the parent node of this test Node 

190 """ 

191 if "config" in kw: 

192 raise TypeError("config is not a valid argument for from_parent") 

193 if "session" in kw: 

194 raise TypeError("session is not a valid argument for from_parent") 

195 return cls._create(parent=parent, **kw) 

196 

197 @property 

198 def ihook(self): 

199 """ fspath sensitive hook proxy used to call pytest hooks""" 

200 return self.session.gethookproxy(self.fspath) 

201 

202 def __repr__(self) -> str: 

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

204 

205 def warn(self, warning: "PytestWarning") -> None: 

206 """Issue a warning for this item. 

207 

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

209 

210 :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning. 

211 

212 :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. 

213 

214 Example usage: 

215 

216 .. code-block:: python 

217 

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

219 

220 """ 

221 from _pytest.warning_types import PytestWarning 

222 

223 if not isinstance(warning, PytestWarning): 

224 raise ValueError( 

225 "warning must be an instance of PytestWarning or subclass, got {!r}".format( 

226 warning 

227 ) 

228 ) 

229 path, lineno = get_fslocation_from_item(self) 

230 assert lineno is not None 

231 warnings.warn_explicit( 

232 warning, category=None, filename=str(path), lineno=lineno + 1, 

233 ) 

234 

235 # methods for ordering nodes 

236 @property 

237 def nodeid(self) -> str: 

238 """ a ::-separated string denoting its collection tree address. """ 

239 return self._nodeid 

240 

241 def __hash__(self) -> int: 

242 return hash(self._nodeid) 

243 

244 def setup(self) -> None: 

245 pass 

246 

247 def teardown(self) -> None: 

248 pass 

249 

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

251 """ return list of all parent collectors up to self, 

252 starting from root of collection tree. """ 

253 chain = [] 

254 item = self # type: Optional[Node] 

255 while item is not None: 

256 chain.append(item) 

257 item = item.parent 

258 chain.reverse() 

259 return chain 

260 

261 def add_marker( 

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

263 ) -> None: 

264 """dynamically add a marker object to the node. 

265 

266 :type marker: ``str`` or ``pytest.mark.*`` object 

267 :param marker: 

268 ``append=True`` whether to append the marker, 

269 if ``False`` insert at position ``0``. 

270 """ 

271 from _pytest.mark import MARK_GEN 

272 

273 if isinstance(marker, MarkDecorator): 

274 marker_ = marker 

275 elif isinstance(marker, str): 

276 marker_ = getattr(MARK_GEN, marker) 

277 else: 

278 raise ValueError("is not a string or pytest.mark.* Marker") 

279 self.keywords[marker_.name] = marker_ 

280 if append: 

281 self.own_markers.append(marker_.mark) 

282 else: 

283 self.own_markers.insert(0, marker_.mark) 

284 

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

286 """ 

287 :param name: if given, filter the results by the name attribute 

288 

289 iterate over all markers of the node 

290 """ 

291 return (x[1] for x in self.iter_markers_with_node(name=name)) 

292 

293 def iter_markers_with_node( 

294 self, name: Optional[str] = None 

295 ) -> Iterator[Tuple["Node", Mark]]: 

296 """ 

297 :param name: if given, filter the results by the name attribute 

298 

299 iterate over all markers of the node 

300 returns sequence of tuples (node, mark) 

301 """ 

302 for node in reversed(self.listchain()): 

303 for mark in node.own_markers: 

304 if name is None or getattr(mark, "name", None) == name: 

305 yield node, mark 

306 

307 @overload 

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

309 raise NotImplementedError() 

310 

311 @overload # noqa: F811 

312 def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811 

313 raise NotImplementedError() 

314 

315 def get_closest_marker( # noqa: F811 

316 self, name: str, default: Optional[Mark] = None 

317 ) -> Optional[Mark]: 

318 """return the first marker matching the name, from closest (for example function) to farther level (for example 

319 module level). 

320 

321 :param default: fallback return value of no marker was found 

322 :param name: name to filter by 

323 """ 

324 return next(self.iter_markers(name=name), default) 

325 

326 def listextrakeywords(self) -> Set[str]: 

327 """ Return a set of all extra keywords in self and any parents.""" 

328 extra_keywords = set() # type: Set[str] 

329 for item in self.listchain(): 

330 extra_keywords.update(item.extra_keyword_matches) 

331 return extra_keywords 

332 

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

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

335 

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

337 """ register a function to be called when this node is finalized. 

338 

339 This method can only be called when this node is active 

340 in a setup chain, for example during self.setup(). 

341 """ 

342 self.session._setupstate.addfinalizer(fin, self) 

343 

344 def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]: 

345 """ get the next parent node (including ourself) 

346 which is an instance of the given class""" 

347 current = self # type: Optional[Node] 

348 while current and not isinstance(current, cls): 

349 current = current.parent 

350 assert current is None or isinstance(current, cls) 

351 return current 

352 

353 def _prunetraceback(self, excinfo): 

354 pass 

355 

356 def _repr_failure_py( 

357 self, 

358 excinfo: ExceptionInfo[BaseException], 

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

360 ) -> TerminalRepr: 

361 if isinstance(excinfo.value, ConftestImportFailure): 

362 excinfo = ExceptionInfo(excinfo.value.excinfo) 

363 if isinstance(excinfo.value, fail.Exception): 

364 if not excinfo.value.pytrace: 

365 style = "value" 

366 if isinstance(excinfo.value, FixtureLookupError): 

367 return excinfo.value.formatrepr() 

368 if self.config.getoption("fulltrace", False): 

369 style = "long" 

370 else: 

371 tb = _pytest._code.Traceback([excinfo.traceback[-1]]) 

372 self._prunetraceback(excinfo) 

373 if len(excinfo.traceback) == 0: 

374 excinfo.traceback = tb 

375 if style == "auto": 

376 style = "long" 

377 # XXX should excinfo.getrepr record all data and toterminal() process it? 

378 if style is None: 

379 if self.config.getoption("tbstyle", "auto") == "short": 

380 style = "short" 

381 else: 

382 style = "long" 

383 

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

385 truncate_locals = False 

386 else: 

387 truncate_locals = True 

388 

389 # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. 

390 # It is possible for a fixture/test to change the CWD while this code runs, which 

391 # would then result in the user seeing confusing paths in the failure message. 

392 # To fix this, if the CWD changed, always display the full absolute path. 

393 # It will be better to just always display paths relative to invocation_dir, but 

394 # this requires a lot of plumbing (#6428). 

395 try: 

396 abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir)) 

397 except OSError: 

398 abspath = True 

399 

400 return excinfo.getrepr( 

401 funcargs=True, 

402 abspath=abspath, 

403 showlocals=self.config.getoption("showlocals", False), 

404 style=style, 

405 tbfilter=False, # pruned already, or in --fulltrace mode. 

406 truncate_locals=truncate_locals, 

407 ) 

408 

409 def repr_failure( 

410 self, 

411 excinfo: ExceptionInfo[BaseException], 

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

413 ) -> Union[str, TerminalRepr]: 

414 """ 

415 Return a representation of a collection or test failure. 

416 

417 :param excinfo: Exception information for the failure. 

418 """ 

419 return self._repr_failure_py(excinfo, style) 

420 

421 

422def get_fslocation_from_item( 

423 node: "Node", 

424) -> Tuple[Union[str, py.path.local], Optional[int]]: 

425 """Tries to extract the actual location from a node, depending on available attributes: 

426 

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

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

429 * "fspath": just a path 

430 

431 :rtype: a tuple of (str|LocalPath, int) with filename and line number. 

432 """ 

433 # See Item.location. 

434 location = getattr( 

435 node, "location", None 

436 ) # type: Optional[Tuple[str, Optional[int], str]] 

437 if location is not None: 

438 return location[:2] 

439 obj = getattr(node, "obj", None) 

440 if obj is not None: 

441 return getfslineno(obj) 

442 return getattr(node, "fspath", "unknown location"), -1 

443 

444 

445class Collector(Node): 

446 """ Collector instances create children through collect() 

447 and thus iteratively build a tree. 

448 """ 

449 

450 class CollectError(Exception): 

451 """ an error during collection, contains a custom message. """ 

452 

453 def collect(self) -> Iterable[Union["Item", "Collector"]]: 

454 """ returns a list of children (items and collectors) 

455 for this collection node. 

456 """ 

457 raise NotImplementedError("abstract") 

458 

459 # TODO: This omits the style= parameter which breaks Liskov Substitution. 

460 def repr_failure( # type: ignore[override] 

461 self, excinfo: ExceptionInfo[BaseException] 

462 ) -> Union[str, TerminalRepr]: 

463 """ 

464 Return a representation of a collection failure. 

465 

466 :param excinfo: Exception information for the failure. 

467 """ 

468 if isinstance(excinfo.value, self.CollectError) and not self.config.getoption( 

469 "fulltrace", False 

470 ): 

471 exc = excinfo.value 

472 return str(exc.args[0]) 

473 

474 # Respect explicit tbstyle option, but default to "short" 

475 # (_repr_failure_py uses "long" with "fulltrace" option always). 

476 tbstyle = self.config.getoption("tbstyle", "auto") 

477 if tbstyle == "auto": 

478 tbstyle = "short" 

479 

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

481 

482 def _prunetraceback(self, excinfo): 

483 if hasattr(self, "fspath"): 

484 traceback = excinfo.traceback 

485 ntraceback = traceback.cut(path=self.fspath) 

486 if ntraceback == traceback: 

487 ntraceback = ntraceback.cut(excludepath=tracebackcutdir) 

488 excinfo.traceback = ntraceback.filter() 

489 

490 

491def _check_initialpaths_for_relpath(session, fspath): 

492 for initial_path in session._initialpaths: 

493 if fspath.common(initial_path) == initial_path: 

494 return fspath.relto(initial_path) 

495 

496 

497class FSHookProxy: 

498 def __init__(self, pm: PytestPluginManager, remove_mods) -> None: 

499 self.pm = pm 

500 self.remove_mods = remove_mods 

501 

502 def __getattr__(self, name: str): 

503 x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) 

504 self.__dict__[name] = x 

505 return x 

506 

507 

508class FSCollector(Collector): 

509 def __init__( 

510 self, 

511 fspath: py.path.local, 

512 parent=None, 

513 config: Optional[Config] = None, 

514 session: Optional["Session"] = None, 

515 nodeid: Optional[str] = None, 

516 ) -> None: 

517 name = fspath.basename 

518 if parent is not None: 

519 rel = fspath.relto(parent.fspath) 

520 if rel: 

521 name = rel 

522 name = name.replace(os.sep, SEP) 

523 self.fspath = fspath 

524 

525 session = session or parent.session 

526 

527 if nodeid is None: 

528 nodeid = self.fspath.relto(session.config.rootdir) 

529 

530 if not nodeid: 

531 nodeid = _check_initialpaths_for_relpath(session, fspath) 

532 if nodeid and os.sep != SEP: 

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

534 

535 super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) 

536 

537 self._norecursepatterns = self.config.getini("norecursedirs") 

538 

539 @classmethod 

540 def from_parent(cls, parent, *, fspath, **kw): 

541 """ 

542 The public constructor 

543 """ 

544 return super().from_parent(parent=parent, fspath=fspath, **kw) 

545 

546 def _gethookproxy(self, fspath: py.path.local): 

547 # check if we have the common case of running 

548 # hooks with all conftest.py files 

549 pm = self.config.pluginmanager 

550 my_conftestmodules = pm._getconftestmodules( 

551 fspath, self.config.getoption("importmode") 

552 ) 

553 remove_mods = pm._conftest_plugins.difference(my_conftestmodules) 

554 if remove_mods: 

555 # one or more conftests are not in use at this fspath 

556 proxy = FSHookProxy(pm, remove_mods) 

557 else: 

558 # all plugins are active for this fspath 

559 proxy = self.config.hook 

560 return proxy 

561 

562 def gethookproxy(self, fspath: py.path.local): 

563 raise NotImplementedError() 

564 

565 def _recurse(self, dirpath: py.path.local) -> bool: 

566 if dirpath.basename == "__pycache__": 

567 return False 

568 ihook = self._gethookproxy(dirpath.dirpath()) 

569 if ihook.pytest_ignore_collect(path=dirpath, config=self.config): 

570 return False 

571 for pat in self._norecursepatterns: 

572 if dirpath.check(fnmatch=pat): 

573 return False 

574 ihook = self._gethookproxy(dirpath) 

575 ihook.pytest_collect_directory(path=dirpath, parent=self) 

576 return True 

577 

578 def isinitpath(self, path: py.path.local) -> bool: 

579 raise NotImplementedError() 

580 

581 def _collectfile( 

582 self, path: py.path.local, handle_dupes: bool = True 

583 ) -> Sequence[Collector]: 

584 assert ( 

585 path.isfile() 

586 ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( 

587 path, path.isdir(), path.exists(), path.islink() 

588 ) 

589 ihook = self.gethookproxy(path) 

590 if not self.isinitpath(path): 

591 if ihook.pytest_ignore_collect(path=path, config=self.config): 

592 return () 

593 

594 if handle_dupes: 

595 keepduplicates = self.config.getoption("keepduplicates") 

596 if not keepduplicates: 

597 duplicate_paths = self.config.pluginmanager._duplicatepaths 

598 if path in duplicate_paths: 

599 return () 

600 else: 

601 duplicate_paths.add(path) 

602 

603 return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] 

604 

605 

606class File(FSCollector): 

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

608 

609 :ref:`non-python tests`. 

610 """ 

611 

612 

613class Item(Node): 

614 """ a basic test invocation item. Note that for a single function 

615 there might be multiple test invocation items. 

616 """ 

617 

618 nextitem = None 

619 

620 def __init__( 

621 self, 

622 name, 

623 parent=None, 

624 config: Optional[Config] = None, 

625 session: Optional["Session"] = None, 

626 nodeid: Optional[str] = None, 

627 ) -> None: 

628 super().__init__(name, parent, config, session, nodeid=nodeid) 

629 self._report_sections = [] # type: List[Tuple[str, str, str]] 

630 

631 #: user properties is a list of tuples (name, value) that holds user 

632 #: defined properties for this test. 

633 self.user_properties = [] # type: List[Tuple[str, object]] 

634 

635 def runtest(self) -> None: 

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

637 

638 def add_report_section(self, when: str, key: str, content: str) -> None: 

639 """ 

640 Adds a new report section, similar to what's done internally to add stdout and 

641 stderr captured output:: 

642 

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

644 

645 :param str when: 

646 One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. 

647 :param str key: 

648 Name of the section, can be customized at will. Pytest uses ``"stdout"`` and 

649 ``"stderr"`` internally. 

650 

651 :param str content: 

652 The full contents as a string. 

653 """ 

654 if content: 

655 self._report_sections.append((when, key, content)) 

656 

657 def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: 

658 return self.fspath, None, "" 

659 

660 @cached_property 

661 def location(self) -> Tuple[str, Optional[int], str]: 

662 location = self.reportinfo() 

663 if isinstance(location[0], py.path.local): 

664 fspath = location[0] 

665 else: 

666 fspath = py.path.local(location[0]) 

667 relfspath = self.session._node_location_to_relpath(fspath) 

668 assert type(location[2]) is str 

669 return (relfspath, location[1], location[2])