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

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 inspect
2import re
3import sys
4import traceback
5from inspect import CO_VARARGS
6from inspect import CO_VARKEYWORDS
7from io import StringIO
8from traceback import format_exception_only
9from types import CodeType
10from types import FrameType
11from types import TracebackType
12from typing import Any
13from typing import Callable
14from typing import Dict
15from typing import Generic
16from typing import Iterable
17from typing import List
18from typing import Mapping
19from typing import Optional
20from typing import Pattern
21from typing import Sequence
22from typing import Set
23from typing import Tuple
24from typing import TypeVar
25from typing import Union
26from weakref import ref
28import attr
29import pluggy
30import py
32import _pytest
33from _pytest._code.source import findsource
34from _pytest._code.source import getrawcode
35from _pytest._code.source import getstatementrange_ast
36from _pytest._code.source import Source
37from _pytest._io import TerminalWriter
38from _pytest._io.saferepr import safeformat
39from _pytest._io.saferepr import saferepr
40from _pytest.compat import ATTRS_EQ_FIELD
41from _pytest.compat import get_real_func
42from _pytest.compat import overload
43from _pytest.compat import TYPE_CHECKING
45if TYPE_CHECKING:
46 from typing import Type
47 from typing_extensions import Literal
48 from weakref import ReferenceType
50 _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
53class Code:
54 """Wrapper around Python code objects."""
56 def __init__(self, rawcode) -> None:
57 if not hasattr(rawcode, "co_filename"):
58 rawcode = getrawcode(rawcode)
59 if not isinstance(rawcode, CodeType):
60 raise TypeError("not a code object: {!r}".format(rawcode))
61 self.filename = rawcode.co_filename
62 self.firstlineno = rawcode.co_firstlineno - 1
63 self.name = rawcode.co_name
64 self.raw = rawcode
66 def __eq__(self, other):
67 return self.raw == other.raw
69 # Ignore type because of https://github.com/python/mypy/issues/4266.
70 __hash__ = None # type: ignore
72 @property
73 def path(self) -> Union[py.path.local, str]:
74 """Return a path object pointing to source code (or a str in case
75 of OSError / non-existing file).
76 """
77 if not self.raw.co_filename:
78 return ""
79 try:
80 p = py.path.local(self.raw.co_filename)
81 # maybe don't try this checking
82 if not p.check():
83 raise OSError("py.path check failed.")
84 return p
85 except OSError:
86 # XXX maybe try harder like the weird logic
87 # in the standard lib [linecache.updatecache] does?
88 return self.raw.co_filename
90 @property
91 def fullsource(self) -> Optional["Source"]:
92 """Return a _pytest._code.Source object for the full source file of the code."""
93 full, _ = findsource(self.raw)
94 return full
96 def source(self) -> "Source":
97 """Return a _pytest._code.Source object for the code object's source only."""
98 # return source only for that part of code
99 return Source(self.raw)
101 def getargs(self, var: bool = False) -> Tuple[str, ...]:
102 """Return a tuple with the argument names for the code object.
104 If 'var' is set True also return the names of the variable and
105 keyword arguments when present.
106 """
107 # Handy shortcut for getting args.
108 raw = self.raw
109 argcount = raw.co_argcount
110 if var:
111 argcount += raw.co_flags & CO_VARARGS
112 argcount += raw.co_flags & CO_VARKEYWORDS
113 return raw.co_varnames[:argcount]
116class Frame:
117 """Wrapper around a Python frame holding f_locals and f_globals
118 in which expressions can be evaluated."""
120 def __init__(self, frame: FrameType) -> None:
121 self.lineno = frame.f_lineno - 1
122 self.f_globals = frame.f_globals
123 self.f_locals = frame.f_locals
124 self.raw = frame
125 self.code = Code(frame.f_code)
127 @property
128 def statement(self) -> "Source":
129 """Statement this frame is at."""
130 if self.code.fullsource is None:
131 return Source("")
132 return self.code.fullsource.getstatement(self.lineno)
134 def eval(self, code, **vars):
135 """Evaluate 'code' in the frame.
137 'vars' are optional additional local variables.
139 Returns the result of the evaluation.
140 """
141 f_locals = self.f_locals.copy()
142 f_locals.update(vars)
143 return eval(code, self.f_globals, f_locals)
145 def repr(self, object: object) -> str:
146 """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
147 return saferepr(object)
149 def getargs(self, var: bool = False):
150 """Return a list of tuples (name, value) for all arguments.
152 If 'var' is set True, also include the variable and keyword arguments
153 when present.
154 """
155 retval = []
156 for arg in self.code.getargs(var):
157 try:
158 retval.append((arg, self.f_locals[arg]))
159 except KeyError:
160 pass # this can occur when using Psyco
161 return retval
164class TracebackEntry:
165 """A single entry in a Traceback."""
167 _repr_style = None # type: Optional[Literal["short", "long"]]
168 exprinfo = None
170 def __init__(
171 self,
172 rawentry: TracebackType,
173 excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
174 ) -> None:
175 self._excinfo = excinfo
176 self._rawentry = rawentry
177 self.lineno = rawentry.tb_lineno - 1
179 def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
180 assert mode in ("short", "long")
181 self._repr_style = mode
183 @property
184 def frame(self) -> Frame:
185 return Frame(self._rawentry.tb_frame)
187 @property
188 def relline(self) -> int:
189 return self.lineno - self.frame.code.firstlineno
191 def __repr__(self) -> str:
192 return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
194 @property
195 def statement(self) -> "Source":
196 """_pytest._code.Source object for the current statement."""
197 source = self.frame.code.fullsource
198 assert source is not None
199 return source.getstatement(self.lineno)
201 @property
202 def path(self) -> Union[py.path.local, str]:
203 """Path to the source code."""
204 return self.frame.code.path
206 @property
207 def locals(self) -> Dict[str, Any]:
208 """Locals of underlying frame."""
209 return self.frame.f_locals
211 def getfirstlinesource(self) -> int:
212 return self.frame.code.firstlineno
214 def getsource(self, astcache=None) -> Optional["Source"]:
215 """Return failing source code."""
216 # we use the passed in astcache to not reparse asttrees
217 # within exception info printing
218 source = self.frame.code.fullsource
219 if source is None:
220 return None
221 key = astnode = None
222 if astcache is not None:
223 key = self.frame.code.path
224 if key is not None:
225 astnode = astcache.get(key, None)
226 start = self.getfirstlinesource()
227 try:
228 astnode, _, end = getstatementrange_ast(
229 self.lineno, source, astnode=astnode
230 )
231 except SyntaxError:
232 end = self.lineno + 1
233 else:
234 if key is not None:
235 astcache[key] = astnode
236 return source[start:end]
238 source = property(getsource)
240 def ishidden(self) -> bool:
241 """Return True if the current frame has a var __tracebackhide__
242 resolving to True.
244 If __tracebackhide__ is a callable, it gets called with the
245 ExceptionInfo instance and can decide whether to hide the traceback.
247 Mostly for internal use.
248 """
249 f = self.frame
250 tbh = f.f_locals.get(
251 "__tracebackhide__", f.f_globals.get("__tracebackhide__", False)
252 ) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]]
253 if tbh and callable(tbh):
254 return tbh(None if self._excinfo is None else self._excinfo())
255 return tbh
257 def __str__(self) -> str:
258 name = self.frame.code.name
259 try:
260 line = str(self.statement).lstrip()
261 except KeyboardInterrupt:
262 raise
263 except BaseException:
264 line = "???"
265 # This output does not quite match Python's repr for traceback entries,
266 # but changing it to do so would break certain plugins. See
267 # https://github.com/pytest-dev/pytest/pull/7535/ for details.
268 return " File %r:%d in %s\n %s\n" % (
269 str(self.path),
270 self.lineno + 1,
271 name,
272 line,
273 )
275 @property
276 def name(self) -> str:
277 """co_name of underlying code."""
278 return self.frame.code.raw.co_name
281class Traceback(List[TracebackEntry]):
282 """Traceback objects encapsulate and offer higher level access to Traceback entries."""
284 def __init__(
285 self,
286 tb: Union[TracebackType, Iterable[TracebackEntry]],
287 excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
288 ) -> None:
289 """Initialize from given python traceback object and ExceptionInfo."""
290 self._excinfo = excinfo
291 if isinstance(tb, TracebackType):
293 def f(cur: TracebackType) -> Iterable[TracebackEntry]:
294 cur_ = cur # type: Optional[TracebackType]
295 while cur_ is not None:
296 yield TracebackEntry(cur_, excinfo=excinfo)
297 cur_ = cur_.tb_next
299 super().__init__(f(tb))
300 else:
301 super().__init__(tb)
303 def cut(
304 self,
305 path=None,
306 lineno: Optional[int] = None,
307 firstlineno: Optional[int] = None,
308 excludepath: Optional[py.path.local] = None,
309 ) -> "Traceback":
310 """Return a Traceback instance wrapping part of this Traceback.
312 By providing any combination of path, lineno and firstlineno, the
313 first frame to start the to-be-returned traceback is determined.
315 This allows cutting the first part of a Traceback instance e.g.
316 for formatting reasons (removing some uninteresting bits that deal
317 with handling of the exception/traceback).
318 """
319 for x in self:
320 code = x.frame.code
321 codepath = code.path
322 if (
323 (path is None or codepath == path)
324 and (
325 excludepath is None
326 or not isinstance(codepath, py.path.local)
327 or not codepath.relto(excludepath)
328 )
329 and (lineno is None or x.lineno == lineno)
330 and (firstlineno is None or x.frame.code.firstlineno == firstlineno)
331 ):
332 return Traceback(x._rawentry, self._excinfo)
333 return self
335 @overload
336 def __getitem__(self, key: int) -> TracebackEntry:
337 raise NotImplementedError()
339 @overload # noqa: F811
340 def __getitem__(self, key: slice) -> "Traceback": # noqa: F811
341 raise NotImplementedError()
343 def __getitem__( # noqa: F811
344 self, key: Union[int, slice]
345 ) -> Union[TracebackEntry, "Traceback"]:
346 if isinstance(key, slice):
347 return self.__class__(super().__getitem__(key))
348 else:
349 return super().__getitem__(key)
351 def filter(
352 self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
353 ) -> "Traceback":
354 """Return a Traceback instance with certain items removed
356 fn is a function that gets a single argument, a TracebackEntry
357 instance, and should return True when the item should be added
358 to the Traceback, False when not.
360 By default this removes all the TracebackEntries which are hidden
361 (see ishidden() above).
362 """
363 return Traceback(filter(fn, self), self._excinfo)
365 def getcrashentry(self) -> TracebackEntry:
366 """Return last non-hidden traceback entry that lead to the exception of a traceback."""
367 for i in range(-1, -len(self) - 1, -1):
368 entry = self[i]
369 if not entry.ishidden():
370 return entry
371 return self[-1]
373 def recursionindex(self) -> Optional[int]:
374 """Return the index of the frame/TracebackEntry where recursion originates if
375 appropriate, None if no recursion occurred."""
376 cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]]
377 for i, entry in enumerate(self):
378 # id for the code.raw is needed to work around
379 # the strange metaprogramming in the decorator lib from pypi
380 # which generates code objects that have hash/value equality
381 # XXX needs a test
382 key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
383 # print "checking for recursion at", key
384 values = cache.setdefault(key, [])
385 if values:
386 f = entry.frame
387 loc = f.f_locals
388 for otherloc in values:
389 if f.eval(
390 co_equal,
391 __recursioncache_locals_1=loc,
392 __recursioncache_locals_2=otherloc,
393 ):
394 return i
395 values.append(entry.frame.f_locals)
396 return None
399co_equal = compile(
400 "__recursioncache_locals_1 == __recursioncache_locals_2", "?", "eval"
401)
404_E = TypeVar("_E", bound=BaseException, covariant=True)
407@attr.s(repr=False)
408class ExceptionInfo(Generic[_E]):
409 """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
411 _assert_start_repr = "AssertionError('assert "
413 _excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]])
414 _striptext = attr.ib(type=str, default="")
415 _traceback = attr.ib(type=Optional[Traceback], default=None)
417 @classmethod
418 def from_exc_info(
419 cls,
420 exc_info: Tuple["Type[_E]", "_E", TracebackType],
421 exprinfo: Optional[str] = None,
422 ) -> "ExceptionInfo[_E]":
423 """Returns an ExceptionInfo for an existing exc_info tuple.
425 .. warning::
427 Experimental API
429 :param exprinfo: a text string helping to determine if we should
430 strip ``AssertionError`` from the output, defaults
431 to the exception message/``__str__()``
432 """
433 _striptext = ""
434 if exprinfo is None and isinstance(exc_info[1], AssertionError):
435 exprinfo = getattr(exc_info[1], "msg", None)
436 if exprinfo is None:
437 exprinfo = saferepr(exc_info[1])
438 if exprinfo and exprinfo.startswith(cls._assert_start_repr):
439 _striptext = "AssertionError: "
441 return cls(exc_info, _striptext)
443 @classmethod
444 def from_current(
445 cls, exprinfo: Optional[str] = None
446 ) -> "ExceptionInfo[BaseException]":
447 """Returns an ExceptionInfo matching the current traceback.
449 .. warning::
451 Experimental API
453 :param exprinfo: a text string helping to determine if we should
454 strip ``AssertionError`` from the output, defaults
455 to the exception message/``__str__()``
456 """
457 tup = sys.exc_info()
458 assert tup[0] is not None, "no current exception"
459 assert tup[1] is not None, "no current exception"
460 assert tup[2] is not None, "no current exception"
461 exc_info = (tup[0], tup[1], tup[2])
462 return ExceptionInfo.from_exc_info(exc_info, exprinfo)
464 @classmethod
465 def for_later(cls) -> "ExceptionInfo[_E]":
466 """Return an unfilled ExceptionInfo."""
467 return cls(None)
469 def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
470 """fill an unfilled ExceptionInfo created with for_later()"""
471 assert self._excinfo is None, "ExceptionInfo was already filled"
472 self._excinfo = exc_info
474 @property
475 def type(self) -> "Type[_E]":
476 """The exception class."""
477 assert (
478 self._excinfo is not None
479 ), ".type can only be used after the context manager exits"
480 return self._excinfo[0]
482 @property
483 def value(self) -> _E:
484 """The exception value."""
485 assert (
486 self._excinfo is not None
487 ), ".value can only be used after the context manager exits"
488 return self._excinfo[1]
490 @property
491 def tb(self) -> TracebackType:
492 """The exception raw traceback."""
493 assert (
494 self._excinfo is not None
495 ), ".tb can only be used after the context manager exits"
496 return self._excinfo[2]
498 @property
499 def typename(self) -> str:
500 """The type name of the exception."""
501 assert (
502 self._excinfo is not None
503 ), ".typename can only be used after the context manager exits"
504 return self.type.__name__
506 @property
507 def traceback(self) -> Traceback:
508 """The traceback."""
509 if self._traceback is None:
510 self._traceback = Traceback(self.tb, excinfo=ref(self))
511 return self._traceback
513 @traceback.setter
514 def traceback(self, value: Traceback) -> None:
515 self._traceback = value
517 def __repr__(self) -> str:
518 if self._excinfo is None:
519 return "<ExceptionInfo for raises contextmanager>"
520 return "<{} {} tblen={}>".format(
521 self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
522 )
524 def exconly(self, tryshort: bool = False) -> str:
525 """Return the exception as a string.
527 When 'tryshort' resolves to True, and the exception is a
528 _pytest._code._AssertionError, only the actual exception part of
529 the exception representation is returned (so 'AssertionError: ' is
530 removed from the beginning).
531 """
532 lines = format_exception_only(self.type, self.value)
533 text = "".join(lines)
534 text = text.rstrip()
535 if tryshort:
536 if text.startswith(self._striptext):
537 text = text[len(self._striptext) :]
538 return text
540 def errisinstance(
541 self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]]
542 ) -> bool:
543 """Return True if the exception is an instance of exc.
545 Consider using ``isinstance(excinfo.value, exc)`` instead.
546 """
547 return isinstance(self.value, exc)
549 def _getreprcrash(self) -> "ReprFileLocation":
550 exconly = self.exconly(tryshort=True)
551 entry = self.traceback.getcrashentry()
552 path, lineno = entry.frame.code.raw.co_filename, entry.lineno
553 return ReprFileLocation(path, lineno + 1, exconly)
555 def getrepr(
556 self,
557 showlocals: bool = False,
558 style: "_TracebackStyle" = "long",
559 abspath: bool = False,
560 tbfilter: bool = True,
561 funcargs: bool = False,
562 truncate_locals: bool = True,
563 chain: bool = True,
564 ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
565 """Return str()able representation of this exception info.
567 :param bool showlocals:
568 Show locals per traceback entry.
569 Ignored if ``style=="native"``.
571 :param str style: long|short|no|native|value traceback style
573 :param bool abspath:
574 If paths should be changed to absolute or left unchanged.
576 :param bool tbfilter:
577 Hide entries that contain a local variable ``__tracebackhide__==True``.
578 Ignored if ``style=="native"``.
580 :param bool funcargs:
581 Show fixtures ("funcargs" for legacy purposes) per traceback entry.
583 :param bool truncate_locals:
584 With ``showlocals==True``, make sure locals can be safely represented as strings.
586 :param bool chain: if chained exceptions in Python 3 should be shown.
588 .. versionchanged:: 3.9
590 Added the ``chain`` parameter.
591 """
592 if style == "native":
593 return ReprExceptionInfo(
594 ReprTracebackNative(
595 traceback.format_exception(
596 self.type, self.value, self.traceback[0]._rawentry
597 )
598 ),
599 self._getreprcrash(),
600 )
602 fmt = FormattedExcinfo(
603 showlocals=showlocals,
604 style=style,
605 abspath=abspath,
606 tbfilter=tbfilter,
607 funcargs=funcargs,
608 truncate_locals=truncate_locals,
609 chain=chain,
610 )
611 return fmt.repr_excinfo(self)
613 def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]":
614 """Check whether the regular expression `regexp` matches the string
615 representation of the exception using :func:`python:re.search`.
617 If it matches `True` is returned, otherwise an `AssertionError` is raised.
618 """
619 __tracebackhide__ = True
620 msg = "Regex pattern {!r} does not match {!r}."
621 if regexp == str(self.value):
622 msg += " Did you mean to `re.escape()` the regex?"
623 assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value))
624 # Return True to allow for "assert excinfo.match()".
625 return True
628@attr.s
629class FormattedExcinfo:
630 """Presenting information about failing Functions and Generators."""
632 # for traceback entries
633 flow_marker = ">"
634 fail_marker = "E"
636 showlocals = attr.ib(type=bool, default=False)
637 style = attr.ib(type="_TracebackStyle", default="long")
638 abspath = attr.ib(type=bool, default=True)
639 tbfilter = attr.ib(type=bool, default=True)
640 funcargs = attr.ib(type=bool, default=False)
641 truncate_locals = attr.ib(type=bool, default=True)
642 chain = attr.ib(type=bool, default=True)
643 astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
645 def _getindent(self, source: "Source") -> int:
646 # figure out indent for given source
647 try:
648 s = str(source.getstatement(len(source) - 1))
649 except KeyboardInterrupt:
650 raise
651 except BaseException:
652 try:
653 s = str(source[-1])
654 except KeyboardInterrupt:
655 raise
656 except BaseException:
657 return 0
658 return 4 + (len(s) - len(s.lstrip()))
660 def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
661 source = entry.getsource(self.astcache)
662 if source is not None:
663 source = source.deindent()
664 return source
666 def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
667 if self.funcargs:
668 args = []
669 for argname, argvalue in entry.frame.getargs(var=True):
670 args.append((argname, saferepr(argvalue)))
671 return ReprFuncArgs(args)
672 return None
674 def get_source(
675 self,
676 source: "Source",
677 line_index: int = -1,
678 excinfo: Optional[ExceptionInfo] = None,
679 short: bool = False,
680 ) -> List[str]:
681 """Return formatted and marked up source lines."""
682 lines = []
683 if source is None or line_index >= len(source.lines):
684 source = Source("???")
685 line_index = 0
686 if line_index < 0:
687 line_index += len(source)
688 space_prefix = " "
689 if short:
690 lines.append(space_prefix + source.lines[line_index].strip())
691 else:
692 for line in source.lines[:line_index]:
693 lines.append(space_prefix + line)
694 lines.append(self.flow_marker + " " + source.lines[line_index])
695 for line in source.lines[line_index + 1 :]:
696 lines.append(space_prefix + line)
697 if excinfo is not None:
698 indent = 4 if short else self._getindent(source)
699 lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
700 return lines
702 def get_exconly(
703 self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False
704 ) -> List[str]:
705 lines = []
706 indentstr = " " * indent
707 # get the real exception information out
708 exlines = excinfo.exconly(tryshort=True).split("\n")
709 failindent = self.fail_marker + indentstr[1:]
710 for line in exlines:
711 lines.append(failindent + line)
712 if not markall:
713 failindent = indentstr
714 return lines
716 def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
717 if self.showlocals:
718 lines = []
719 keys = [loc for loc in locals if loc[0] != "@"]
720 keys.sort()
721 for name in keys:
722 value = locals[name]
723 if name == "__builtins__":
724 lines.append("__builtins__ = <builtins>")
725 else:
726 # This formatting could all be handled by the
727 # _repr() function, which is only reprlib.Repr in
728 # disguise, so is very configurable.
729 if self.truncate_locals:
730 str_repr = saferepr(value)
731 else:
732 str_repr = safeformat(value)
733 # if len(str_repr) < 70 or not isinstance(value,
734 # (list, tuple, dict)):
735 lines.append("{:<10} = {}".format(name, str_repr))
736 # else:
737 # self._line("%-10s =\\" % (name,))
738 # # XXX
739 # pprint.pprint(value, stream=self.excinfowriter)
740 return ReprLocals(lines)
741 return None
743 def repr_traceback_entry(
744 self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None
745 ) -> "ReprEntry":
746 lines = [] # type: List[str]
747 style = entry._repr_style if entry._repr_style is not None else self.style
748 if style in ("short", "long"):
749 source = self._getentrysource(entry)
750 if source is None:
751 source = Source("???")
752 line_index = 0
753 else:
754 line_index = entry.lineno - entry.getfirstlinesource()
755 short = style == "short"
756 reprargs = self.repr_args(entry) if not short else None
757 s = self.get_source(source, line_index, excinfo, short=short)
758 lines.extend(s)
759 if short:
760 message = "in %s" % (entry.name)
761 else:
762 message = excinfo and excinfo.typename or ""
763 path = self._makepath(entry.path)
764 reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
765 localsrepr = self.repr_locals(entry.locals)
766 return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
767 elif style == "value":
768 if excinfo:
769 lines.extend(str(excinfo.value).split("\n"))
770 return ReprEntry(lines, None, None, None, style)
771 else:
772 if excinfo:
773 lines.extend(self.get_exconly(excinfo, indent=4))
774 return ReprEntry(lines, None, None, None, style)
776 def _makepath(self, path):
777 if not self.abspath:
778 try:
779 np = py.path.local().bestrelpath(path)
780 except OSError:
781 return path
782 if len(np) < len(str(path)):
783 path = np
784 return path
786 def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback":
787 traceback = excinfo.traceback
788 if self.tbfilter:
789 traceback = traceback.filter()
791 if isinstance(excinfo.value, RecursionError):
792 traceback, extraline = self._truncate_recursive_traceback(traceback)
793 else:
794 extraline = None
796 last = traceback[-1]
797 entries = []
798 if self.style == "value":
799 reprentry = self.repr_traceback_entry(last, excinfo)
800 entries.append(reprentry)
801 return ReprTraceback(entries, None, style=self.style)
803 for index, entry in enumerate(traceback):
804 einfo = (last == entry) and excinfo or None
805 reprentry = self.repr_traceback_entry(entry, einfo)
806 entries.append(reprentry)
807 return ReprTraceback(entries, extraline, style=self.style)
809 def _truncate_recursive_traceback(
810 self, traceback: Traceback
811 ) -> Tuple[Traceback, Optional[str]]:
812 """
813 Truncate the given recursive traceback trying to find the starting point
814 of the recursion.
816 The detection is done by going through each traceback entry and finding the
817 point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
819 Handle the situation where the recursion process might raise an exception (for example
820 comparing numpy arrays using equality raises a TypeError), in which case we do our best to
821 warn the user of the error and show a limited traceback.
822 """
823 try:
824 recursionindex = traceback.recursionindex()
825 except Exception as e:
826 max_frames = 10
827 extraline = (
828 "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
829 " The following exception happened when comparing locals in the stack frame:\n"
830 " {exc_type}: {exc_msg}\n"
831 " Displaying first and last {max_frames} stack frames out of {total}."
832 ).format(
833 exc_type=type(e).__name__,
834 exc_msg=str(e),
835 max_frames=max_frames,
836 total=len(traceback),
837 ) # type: Optional[str]
838 # Type ignored because adding two instaces of a List subtype
839 # currently incorrectly has type List instead of the subtype.
840 traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
841 else:
842 if recursionindex is not None:
843 extraline = "!!! Recursion detected (same locals & position)"
844 traceback = traceback[: recursionindex + 1]
845 else:
846 extraline = None
848 return traceback, extraline
850 def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr":
851 repr_chain = (
852 []
853 ) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
854 e = excinfo.value
855 excinfo_ = excinfo # type: Optional[ExceptionInfo]
856 descr = None
857 seen = set() # type: Set[int]
858 while e is not None and id(e) not in seen:
859 seen.add(id(e))
860 if excinfo_:
861 reprtraceback = self.repr_traceback(excinfo_)
862 reprcrash = (
863 excinfo_._getreprcrash() if self.style != "value" else None
864 ) # type: Optional[ReprFileLocation]
865 else:
866 # fallback to native repr if the exception doesn't have a traceback:
867 # ExceptionInfo objects require a full traceback to work
868 reprtraceback = ReprTracebackNative(
869 traceback.format_exception(type(e), e, None)
870 )
871 reprcrash = None
873 repr_chain += [(reprtraceback, reprcrash, descr)]
874 if e.__cause__ is not None and self.chain:
875 e = e.__cause__
876 excinfo_ = (
877 ExceptionInfo((type(e), e, e.__traceback__))
878 if e.__traceback__
879 else None
880 )
881 descr = "The above exception was the direct cause of the following exception:"
882 elif (
883 e.__context__ is not None and not e.__suppress_context__ and self.chain
884 ):
885 e = e.__context__
886 excinfo_ = (
887 ExceptionInfo((type(e), e, e.__traceback__))
888 if e.__traceback__
889 else None
890 )
891 descr = "During handling of the above exception, another exception occurred:"
892 else:
893 e = None
894 repr_chain.reverse()
895 return ExceptionChainRepr(repr_chain)
898@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
899class TerminalRepr:
900 def __str__(self) -> str:
901 # FYI this is called from pytest-xdist's serialization of exception
902 # information.
903 io = StringIO()
904 tw = TerminalWriter(file=io)
905 self.toterminal(tw)
906 return io.getvalue().strip()
908 def __repr__(self) -> str:
909 return "<{} instance at {:0x}>".format(self.__class__, id(self))
911 def toterminal(self, tw: TerminalWriter) -> None:
912 raise NotImplementedError()
915# This class is abstract -- only subclasses are instantiated.
916@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
917class ExceptionRepr(TerminalRepr):
918 # Provided by in subclasses.
919 reprcrash = None # type: Optional[ReprFileLocation]
920 reprtraceback = None # type: ReprTraceback
922 def __attrs_post_init__(self) -> None:
923 self.sections = [] # type: List[Tuple[str, str, str]]
925 def addsection(self, name: str, content: str, sep: str = "-") -> None:
926 self.sections.append((name, content, sep))
928 def toterminal(self, tw: TerminalWriter) -> None:
929 for name, content, sep in self.sections:
930 tw.sep(sep, name)
931 tw.line(content)
934@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
935class ExceptionChainRepr(ExceptionRepr):
936 chain = attr.ib(
937 type=Sequence[
938 Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
939 ]
940 )
942 def __attrs_post_init__(self) -> None:
943 super().__attrs_post_init__()
944 # reprcrash and reprtraceback of the outermost (the newest) exception
945 # in the chain
946 self.reprtraceback = self.chain[-1][0]
947 self.reprcrash = self.chain[-1][1]
949 def toterminal(self, tw: TerminalWriter) -> None:
950 for element in self.chain:
951 element[0].toterminal(tw)
952 if element[2] is not None:
953 tw.line("")
954 tw.line(element[2], yellow=True)
955 super().toterminal(tw)
958@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
959class ReprExceptionInfo(ExceptionRepr):
960 reprtraceback = attr.ib(type="ReprTraceback")
961 reprcrash = attr.ib(type="ReprFileLocation")
963 def toterminal(self, tw: TerminalWriter) -> None:
964 self.reprtraceback.toterminal(tw)
965 super().toterminal(tw)
968@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
969class ReprTraceback(TerminalRepr):
970 reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
971 extraline = attr.ib(type=Optional[str])
972 style = attr.ib(type="_TracebackStyle")
974 entrysep = "_ "
976 def toterminal(self, tw: TerminalWriter) -> None:
977 # the entries might have different styles
978 for i, entry in enumerate(self.reprentries):
979 if entry.style == "long":
980 tw.line("")
981 entry.toterminal(tw)
982 if i < len(self.reprentries) - 1:
983 next_entry = self.reprentries[i + 1]
984 if (
985 entry.style == "long"
986 or entry.style == "short"
987 and next_entry.style == "long"
988 ):
989 tw.sep(self.entrysep)
991 if self.extraline:
992 tw.line(self.extraline)
995class ReprTracebackNative(ReprTraceback):
996 def __init__(self, tblines: Sequence[str]) -> None:
997 self.style = "native"
998 self.reprentries = [ReprEntryNative(tblines)]
999 self.extraline = None
1002@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
1003class ReprEntryNative(TerminalRepr):
1004 lines = attr.ib(type=Sequence[str])
1005 style = "native" # type: _TracebackStyle
1007 def toterminal(self, tw: TerminalWriter) -> None:
1008 tw.write("".join(self.lines))
1011@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
1012class ReprEntry(TerminalRepr):
1013 lines = attr.ib(type=Sequence[str])
1014 reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"])
1015 reprlocals = attr.ib(type=Optional["ReprLocals"])
1016 reprfileloc = attr.ib(type=Optional["ReprFileLocation"])
1017 style = attr.ib(type="_TracebackStyle")
1019 def _write_entry_lines(self, tw: TerminalWriter) -> None:
1020 """Writes the source code portions of a list of traceback entries with syntax highlighting.
1022 Usually entries are lines like these:
1024 " x = 1"
1025 "> assert x == 2"
1026 "E assert 1 == 2"
1028 This function takes care of rendering the "source" portions of it (the lines without
1029 the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
1030 character, as doing so might break line continuations.
1031 """
1033 if not self.lines:
1034 return
1036 # separate indents and source lines that are not failures: we want to
1037 # highlight the code but not the indentation, which may contain markers
1038 # such as "> assert 0"
1039 fail_marker = "{} ".format(FormattedExcinfo.fail_marker)
1040 indent_size = len(fail_marker)
1041 indents = [] # type: List[str]
1042 source_lines = [] # type: List[str]
1043 failure_lines = [] # type: List[str]
1044 for index, line in enumerate(self.lines):
1045 is_failure_line = line.startswith(fail_marker)
1046 if is_failure_line:
1047 # from this point on all lines are considered part of the failure
1048 failure_lines.extend(self.lines[index:])
1049 break
1050 else:
1051 if self.style == "value":
1052 source_lines.append(line)
1053 else:
1054 indents.append(line[:indent_size])
1055 source_lines.append(line[indent_size:])
1057 tw._write_source(source_lines, indents)
1059 # failure lines are always completely red and bold
1060 for line in failure_lines:
1061 tw.line(line, bold=True, red=True)
1063 def toterminal(self, tw: TerminalWriter) -> None:
1064 if self.style == "short":
1065 assert self.reprfileloc is not None
1066 self.reprfileloc.toterminal(tw)
1067 self._write_entry_lines(tw)
1068 if self.reprlocals:
1069 self.reprlocals.toterminal(tw, indent=" " * 8)
1070 return
1072 if self.reprfuncargs:
1073 self.reprfuncargs.toterminal(tw)
1075 self._write_entry_lines(tw)
1077 if self.reprlocals:
1078 tw.line("")
1079 self.reprlocals.toterminal(tw)
1080 if self.reprfileloc:
1081 if self.lines:
1082 tw.line("")
1083 self.reprfileloc.toterminal(tw)
1085 def __str__(self) -> str:
1086 return "{}\n{}\n{}".format(
1087 "\n".join(self.lines), self.reprlocals, self.reprfileloc
1088 )
1091@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
1092class ReprFileLocation(TerminalRepr):
1093 path = attr.ib(type=str, converter=str)
1094 lineno = attr.ib(type=int)
1095 message = attr.ib(type=str)
1097 def toterminal(self, tw: TerminalWriter) -> None:
1098 # filename and lineno output for each entry,
1099 # using an output format that most editors understand
1100 msg = self.message
1101 i = msg.find("\n")
1102 if i != -1:
1103 msg = msg[:i]
1104 tw.write(self.path, bold=True, red=True)
1105 tw.line(":{}: {}".format(self.lineno, msg))
1108@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
1109class ReprLocals(TerminalRepr):
1110 lines = attr.ib(type=Sequence[str])
1112 def toterminal(self, tw: TerminalWriter, indent="") -> None:
1113 for line in self.lines:
1114 tw.line(indent + line)
1117@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
1118class ReprFuncArgs(TerminalRepr):
1119 args = attr.ib(type=Sequence[Tuple[str, object]])
1121 def toterminal(self, tw: TerminalWriter) -> None:
1122 if self.args:
1123 linesofar = ""
1124 for name, value in self.args:
1125 ns = "{} = {}".format(name, value)
1126 if len(ns) + len(linesofar) + 2 > tw.fullwidth:
1127 if linesofar:
1128 tw.line(linesofar)
1129 linesofar = ns
1130 else:
1131 if linesofar:
1132 linesofar += ", " + ns
1133 else:
1134 linesofar = ns
1135 if linesofar:
1136 tw.line(linesofar)
1137 tw.line("")
1140def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]:
1141 """Return source location (path, lineno) for the given object.
1143 If the source cannot be determined return ("", -1).
1145 The line number is 0-based.
1146 """
1147 # xxx let decorators etc specify a sane ordering
1148 # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
1149 # in 6ec13a2b9. It ("place_as") appears to be something very custom.
1150 obj = get_real_func(obj)
1151 if hasattr(obj, "place_as"):
1152 obj = obj.place_as # type: ignore[attr-defined]
1154 try:
1155 code = Code(obj)
1156 except TypeError:
1157 try:
1158 fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
1159 except TypeError:
1160 return "", -1
1162 fspath = fn and py.path.local(fn) or ""
1163 lineno = -1
1164 if fspath:
1165 try:
1166 _, lineno = findsource(obj)
1167 except OSError:
1168 pass
1169 return fspath, lineno
1171 return code.path, code.firstlineno
1174# relative paths that we use to filter traceback entries from appearing to the user;
1175# see filter_traceback
1176# note: if we need to add more paths than what we have now we should probably use a list
1177# for better maintenance
1179_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
1180# pluggy is either a package or a single module depending on the version
1181if _PLUGGY_DIR.basename == "__init__.py":
1182 _PLUGGY_DIR = _PLUGGY_DIR.dirpath()
1183_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath()
1184_PY_DIR = py.path.local(py.__file__).dirpath()
1187def filter_traceback(entry: TracebackEntry) -> bool:
1188 """Return True if a TracebackEntry instance should be included in tracebacks.
1190 We hide traceback entries of:
1192 * dynamically generated code (no code to show up for it);
1193 * internal traceback from pytest or its internal libraries, py and pluggy.
1194 """
1195 # entry.path might sometimes return a str object when the entry
1196 # points to dynamically generated code
1197 # see https://bitbucket.org/pytest-dev/py/issues/71
1198 raw_filename = entry.frame.code.raw.co_filename
1199 is_generated = "<" in raw_filename and ">" in raw_filename
1200 if is_generated:
1201 return False
1202 # entry.path might point to a non-existing file, in which case it will
1203 # also return a str object. see #1133
1204 p = py.path.local(entry.path)
1205 return (
1206 not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
1207 )