Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/_pytest/nodes.py : 36%

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
16import py
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
38if TYPE_CHECKING:
39 from typing import Type
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
47SEP = "/"
49tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
52@lru_cache(maxsize=None)
53def _splitnode(nodeid: str) -> Tuple[str, ...]:
54 """Split a nodeid into constituent 'parts'.
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'
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)
78def ischildnode(baseid: str, nodeid: str) -> bool:
79 """Return True if the nodeid is a child node of the baseid.
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
90_NodeType = TypeVar("_NodeType", bound="Node")
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)
98 def _create(self, *k, **kw):
99 return super().__call__(*k, **kw)
102class Node(metaclass=NodeMeta):
103 """ base class for Collector and Item the test collection tree.
104 Collector subclasses have children, Items are terminal nodes."""
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 )
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
131 #: the parent collector node.
132 self.parent = parent
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
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
150 #: filesystem path where this node was collected from (can be None)
151 self.fspath = fspath or getattr(parent, "fspath", None)
153 #: keywords/markers collected from all scopes
154 self.keywords = NodeKeywords(self)
156 #: the marker objects belonging to this node
157 self.own_markers = [] # type: List[Mark]
159 #: allow adding of extra keywords to use for matching
160 self.extra_keyword_matches = set() # type: Set[str]
162 # used for storing artificial fixturedefs for direct parametrization
163 self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef]
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
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()
179 @classmethod
180 def from_parent(cls, parent: "Node", **kw):
181 """
182 Public Constructor for Nodes
184 This indirection got introduced in order to enable removing
185 the fragile logic from the node constructors.
187 Subclasses can use ``super().from_parent(...)`` when overriding the construction
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)
197 @property
198 def ihook(self):
199 """ fspath sensitive hook proxy used to call pytest hooks"""
200 return self.session.gethookproxy(self.fspath)
202 def __repr__(self) -> str:
203 return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
205 def warn(self, warning: "PytestWarning") -> None:
206 """Issue a warning for this item.
208 Warnings will be displayed after the test session, unless explicitly suppressed
210 :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
212 :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
214 Example usage:
216 .. code-block:: python
218 node.warn(PytestWarning("some message"))
220 """
221 from _pytest.warning_types import PytestWarning
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 )
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
241 def __hash__(self) -> int:
242 return hash(self._nodeid)
244 def setup(self) -> None:
245 pass
247 def teardown(self) -> None:
248 pass
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
261 def add_marker(
262 self, marker: Union[str, MarkDecorator], append: bool = True
263 ) -> None:
264 """dynamically add a marker object to the node.
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
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)
285 def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
286 """
287 :param name: if given, filter the results by the name attribute
289 iterate over all markers of the node
290 """
291 return (x[1] for x in self.iter_markers_with_node(name=name))
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
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
307 @overload
308 def get_closest_marker(self, name: str) -> Optional[Mark]:
309 raise NotImplementedError()
311 @overload # noqa: F811
312 def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811
313 raise NotImplementedError()
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).
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)
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
333 def listnames(self) -> List[str]:
334 return [x.name for x in self.listchain()]
336 def addfinalizer(self, fin: Callable[[], object]) -> None:
337 """ register a function to be called when this node is finalized.
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)
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
353 def _prunetraceback(self, excinfo):
354 pass
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"
384 if self.config.getoption("verbose", 0) > 1:
385 truncate_locals = False
386 else:
387 truncate_locals = True
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
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 )
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.
417 :param excinfo: Exception information for the failure.
418 """
419 return self._repr_failure_py(excinfo, style)
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:
427 * "location": a pair (path, lineno)
428 * "obj": a Python object that the node wraps.
429 * "fspath": just a path
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
445class Collector(Node):
446 """ Collector instances create children through collect()
447 and thus iteratively build a tree.
448 """
450 class CollectError(Exception):
451 """ an error during collection, contains a custom message. """
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")
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.
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])
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"
480 return self._repr_failure_py(excinfo, style=tbstyle)
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()
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)
497class FSHookProxy:
498 def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
499 self.pm = pm
500 self.remove_mods = remove_mods
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
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
525 session = session or parent.session
527 if nodeid is None:
528 nodeid = self.fspath.relto(session.config.rootdir)
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)
535 super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath)
537 self._norecursepatterns = self.config.getini("norecursedirs")
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)
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
562 def gethookproxy(self, fspath: py.path.local):
563 raise NotImplementedError()
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
578 def isinitpath(self, path: py.path.local) -> bool:
579 raise NotImplementedError()
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 ()
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)
603 return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return]
606class File(FSCollector):
607 """Base class for collecting tests from a file.
609 :ref:`non-python tests`.
610 """
613class Item(Node):
614 """ a basic test invocation item. Note that for a single function
615 there might be multiple test invocation items.
616 """
618 nextitem = None
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]]
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]]
635 def runtest(self) -> None:
636 raise NotImplementedError("runtest must be implemented by Item subclass")
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::
643 item.add_report_section("call", "stdout", "report section contents")
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.
651 :param str content:
652 The full contents as a string.
653 """
654 if content:
655 self._report_sections.append((when, key, content))
657 def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]:
658 return self.fspath, None, ""
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])