Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/_code/code.py: 36%
721 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1import ast
2import inspect
3import os
4import re
5import sys
6import traceback
7from inspect import CO_VARARGS
8from inspect import CO_VARKEYWORDS
9from io import StringIO
10from pathlib import Path
11from traceback import format_exception_only
12from types import CodeType
13from types import FrameType
14from types import TracebackType
15from typing import Any
16from typing import Callable
17from typing import ClassVar
18from typing import Dict
19from typing import Generic
20from typing import Iterable
21from typing import List
22from typing import Mapping
23from typing import Optional
24from typing import overload
25from typing import Pattern
26from typing import Sequence
27from typing import Set
28from typing import Tuple
29from typing import Type
30from typing import TYPE_CHECKING
31from typing import TypeVar
32from typing import Union
33from weakref import ref
35import attr
36import pluggy
38import _pytest
39from _pytest._code.source import findsource
40from _pytest._code.source import getrawcode
41from _pytest._code.source import getstatementrange_ast
42from _pytest._code.source import Source
43from _pytest._io import TerminalWriter
44from _pytest._io.saferepr import safeformat
45from _pytest._io.saferepr import saferepr
46from _pytest.compat import final
47from _pytest.compat import get_real_func
48from _pytest.deprecated import check_ispytest
49from _pytest.pathlib import absolutepath
50from _pytest.pathlib import bestrelpath
52if TYPE_CHECKING:
53 from typing_extensions import Literal
54 from typing_extensions import SupportsIndex
55 from weakref import ReferenceType
57 _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
59if sys.version_info[:2] < (3, 11):
60 from exceptiongroup import BaseExceptionGroup
63class Code:
64 """Wrapper around Python code objects."""
66 __slots__ = ("raw",)
68 def __init__(self, obj: CodeType) -> None:
69 self.raw = obj
71 @classmethod
72 def from_function(cls, obj: object) -> "Code":
73 return cls(getrawcode(obj))
75 def __eq__(self, other):
76 return self.raw == other.raw
78 # Ignore type because of https://github.com/python/mypy/issues/4266.
79 __hash__ = None # type: ignore
81 @property
82 def firstlineno(self) -> int:
83 return self.raw.co_firstlineno - 1
85 @property
86 def name(self) -> str:
87 return self.raw.co_name
89 @property
90 def path(self) -> Union[Path, str]:
91 """Return a path object pointing to source code, or an ``str`` in
92 case of ``OSError`` / non-existing file."""
93 if not self.raw.co_filename:
94 return ""
95 try:
96 p = absolutepath(self.raw.co_filename)
97 # maybe don't try this checking
98 if not p.exists():
99 raise OSError("path check failed.")
100 return p
101 except OSError:
102 # XXX maybe try harder like the weird logic
103 # in the standard lib [linecache.updatecache] does?
104 return self.raw.co_filename
106 @property
107 def fullsource(self) -> Optional["Source"]:
108 """Return a _pytest._code.Source object for the full source file of the code."""
109 full, _ = findsource(self.raw)
110 return full
112 def source(self) -> "Source":
113 """Return a _pytest._code.Source object for the code object's source only."""
114 # return source only for that part of code
115 return Source(self.raw)
117 def getargs(self, var: bool = False) -> Tuple[str, ...]:
118 """Return a tuple with the argument names for the code object.
120 If 'var' is set True also return the names of the variable and
121 keyword arguments when present.
122 """
123 # Handy shortcut for getting args.
124 raw = self.raw
125 argcount = raw.co_argcount
126 if var:
127 argcount += raw.co_flags & CO_VARARGS
128 argcount += raw.co_flags & CO_VARKEYWORDS
129 return raw.co_varnames[:argcount]
132class Frame:
133 """Wrapper around a Python frame holding f_locals and f_globals
134 in which expressions can be evaluated."""
136 __slots__ = ("raw",)
138 def __init__(self, frame: FrameType) -> None:
139 self.raw = frame
141 @property
142 def lineno(self) -> int:
143 return self.raw.f_lineno - 1
145 @property
146 def f_globals(self) -> Dict[str, Any]:
147 return self.raw.f_globals
149 @property
150 def f_locals(self) -> Dict[str, Any]:
151 return self.raw.f_locals
153 @property
154 def code(self) -> Code:
155 return Code(self.raw.f_code)
157 @property
158 def statement(self) -> "Source":
159 """Statement this frame is at."""
160 if self.code.fullsource is None:
161 return Source("")
162 return self.code.fullsource.getstatement(self.lineno)
164 def eval(self, code, **vars):
165 """Evaluate 'code' in the frame.
167 'vars' are optional additional local variables.
169 Returns the result of the evaluation.
170 """
171 f_locals = self.f_locals.copy()
172 f_locals.update(vars)
173 return eval(code, self.f_globals, f_locals)
175 def repr(self, object: object) -> str:
176 """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
177 return saferepr(object)
179 def getargs(self, var: bool = False):
180 """Return a list of tuples (name, value) for all arguments.
182 If 'var' is set True, also include the variable and keyword arguments
183 when present.
184 """
185 retval = []
186 for arg in self.code.getargs(var):
187 try:
188 retval.append((arg, self.f_locals[arg]))
189 except KeyError:
190 pass # this can occur when using Psyco
191 return retval
194class TracebackEntry:
195 """A single entry in a Traceback."""
197 __slots__ = ("_rawentry", "_excinfo", "_repr_style")
199 def __init__(
200 self,
201 rawentry: TracebackType,
202 excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
203 ) -> None:
204 self._rawentry = rawentry
205 self._excinfo = excinfo
206 self._repr_style: Optional['Literal["short", "long"]'] = None
208 @property
209 def lineno(self) -> int:
210 return self._rawentry.tb_lineno - 1
212 def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
213 assert mode in ("short", "long")
214 self._repr_style = mode
216 @property
217 def frame(self) -> Frame:
218 return Frame(self._rawentry.tb_frame)
220 @property
221 def relline(self) -> int:
222 return self.lineno - self.frame.code.firstlineno
224 def __repr__(self) -> str:
225 return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
227 @property
228 def statement(self) -> "Source":
229 """_pytest._code.Source object for the current statement."""
230 source = self.frame.code.fullsource
231 assert source is not None
232 return source.getstatement(self.lineno)
234 @property
235 def path(self) -> Union[Path, str]:
236 """Path to the source code."""
237 return self.frame.code.path
239 @property
240 def locals(self) -> Dict[str, Any]:
241 """Locals of underlying frame."""
242 return self.frame.f_locals
244 def getfirstlinesource(self) -> int:
245 return self.frame.code.firstlineno
247 def getsource(
248 self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None
249 ) -> Optional["Source"]:
250 """Return failing source code."""
251 # we use the passed in astcache to not reparse asttrees
252 # within exception info printing
253 source = self.frame.code.fullsource
254 if source is None:
255 return None
256 key = astnode = None
257 if astcache is not None:
258 key = self.frame.code.path
259 if key is not None:
260 astnode = astcache.get(key, None)
261 start = self.getfirstlinesource()
262 try:
263 astnode, _, end = getstatementrange_ast(
264 self.lineno, source, astnode=astnode
265 )
266 except SyntaxError:
267 end = self.lineno + 1
268 else:
269 if key is not None and astcache is not None:
270 astcache[key] = astnode
271 return source[start:end]
273 source = property(getsource)
275 def ishidden(self) -> bool:
276 """Return True if the current frame has a var __tracebackhide__
277 resolving to True.
279 If __tracebackhide__ is a callable, it gets called with the
280 ExceptionInfo instance and can decide whether to hide the traceback.
282 Mostly for internal use.
283 """
284 tbh: Union[
285 bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]
286 ] = False
287 for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
288 # in normal cases, f_locals and f_globals are dictionaries
289 # however via `exec(...)` / `eval(...)` they can be other types
290 # (even incorrect types!).
291 # as such, we suppress all exceptions while accessing __tracebackhide__
292 try:
293 tbh = maybe_ns_dct["__tracebackhide__"]
294 except Exception:
295 pass
296 else:
297 break
298 if tbh and callable(tbh):
299 return tbh(None if self._excinfo is None else self._excinfo())
300 return tbh
302 def __str__(self) -> str:
303 name = self.frame.code.name
304 try:
305 line = str(self.statement).lstrip()
306 except KeyboardInterrupt:
307 raise
308 except BaseException:
309 line = "???"
310 # This output does not quite match Python's repr for traceback entries,
311 # but changing it to do so would break certain plugins. See
312 # https://github.com/pytest-dev/pytest/pull/7535/ for details.
313 return " File %r:%d in %s\n %s\n" % (
314 str(self.path),
315 self.lineno + 1,
316 name,
317 line,
318 )
320 @property
321 def name(self) -> str:
322 """co_name of underlying code."""
323 return self.frame.code.raw.co_name
326class Traceback(List[TracebackEntry]):
327 """Traceback objects encapsulate and offer higher level access to Traceback entries."""
329 def __init__(
330 self,
331 tb: Union[TracebackType, Iterable[TracebackEntry]],
332 excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
333 ) -> None:
334 """Initialize from given python traceback object and ExceptionInfo."""
335 self._excinfo = excinfo
336 if isinstance(tb, TracebackType):
338 def f(cur: TracebackType) -> Iterable[TracebackEntry]:
339 cur_: Optional[TracebackType] = cur
340 while cur_ is not None:
341 yield TracebackEntry(cur_, excinfo=excinfo)
342 cur_ = cur_.tb_next
344 super().__init__(f(tb))
345 else:
346 super().__init__(tb)
348 def cut(
349 self,
350 path: Optional[Union["os.PathLike[str]", str]] = None,
351 lineno: Optional[int] = None,
352 firstlineno: Optional[int] = None,
353 excludepath: Optional["os.PathLike[str]"] = None,
354 ) -> "Traceback":
355 """Return a Traceback instance wrapping part of this Traceback.
357 By providing any combination of path, lineno and firstlineno, the
358 first frame to start the to-be-returned traceback is determined.
360 This allows cutting the first part of a Traceback instance e.g.
361 for formatting reasons (removing some uninteresting bits that deal
362 with handling of the exception/traceback).
363 """
364 path_ = None if path is None else os.fspath(path)
365 excludepath_ = None if excludepath is None else os.fspath(excludepath)
366 for x in self:
367 code = x.frame.code
368 codepath = code.path
369 if path is not None and str(codepath) != path_:
370 continue
371 if (
372 excludepath is not None
373 and isinstance(codepath, Path)
374 and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
375 ):
376 continue
377 if lineno is not None and x.lineno != lineno:
378 continue
379 if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
380 continue
381 return Traceback(x._rawentry, self._excinfo)
382 return self
384 @overload
385 def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
386 ...
388 @overload
389 def __getitem__(self, key: slice) -> "Traceback":
390 ...
392 def __getitem__(
393 self, key: Union["SupportsIndex", slice]
394 ) -> Union[TracebackEntry, "Traceback"]:
395 if isinstance(key, slice):
396 return self.__class__(super().__getitem__(key))
397 else:
398 return super().__getitem__(key)
400 def filter(
401 self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
402 ) -> "Traceback":
403 """Return a Traceback instance with certain items removed
405 fn is a function that gets a single argument, a TracebackEntry
406 instance, and should return True when the item should be added
407 to the Traceback, False when not.
409 By default this removes all the TracebackEntries which are hidden
410 (see ishidden() above).
411 """
412 return Traceback(filter(fn, self), self._excinfo)
414 def getcrashentry(self) -> TracebackEntry:
415 """Return last non-hidden traceback entry that lead to the exception of a traceback."""
416 for i in range(-1, -len(self) - 1, -1):
417 entry = self[i]
418 if not entry.ishidden():
419 return entry
420 return self[-1]
422 def recursionindex(self) -> Optional[int]:
423 """Return the index of the frame/TracebackEntry where recursion originates if
424 appropriate, None if no recursion occurred."""
425 cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}
426 for i, entry in enumerate(self):
427 # id for the code.raw is needed to work around
428 # the strange metaprogramming in the decorator lib from pypi
429 # which generates code objects that have hash/value equality
430 # XXX needs a test
431 key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
432 # print "checking for recursion at", key
433 values = cache.setdefault(key, [])
434 if values:
435 f = entry.frame
436 loc = f.f_locals
437 for otherloc in values:
438 if otherloc == loc:
439 return i
440 values.append(entry.frame.f_locals)
441 return None
444E = TypeVar("E", bound=BaseException, covariant=True)
447@final
448@attr.s(repr=False, init=False, auto_attribs=True)
449class ExceptionInfo(Generic[E]):
450 """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
452 _assert_start_repr: ClassVar = "AssertionError('assert "
454 _excinfo: Optional[Tuple[Type["E"], "E", TracebackType]]
455 _striptext: str
456 _traceback: Optional[Traceback]
458 def __init__(
459 self,
460 excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
461 striptext: str = "",
462 traceback: Optional[Traceback] = None,
463 *,
464 _ispytest: bool = False,
465 ) -> None:
466 check_ispytest(_ispytest)
467 self._excinfo = excinfo
468 self._striptext = striptext
469 self._traceback = traceback
471 @classmethod
472 def from_exc_info(
473 cls,
474 exc_info: Tuple[Type[E], E, TracebackType],
475 exprinfo: Optional[str] = None,
476 ) -> "ExceptionInfo[E]":
477 """Return an ExceptionInfo for an existing exc_info tuple.
479 .. warning::
481 Experimental API
483 :param exprinfo:
484 A text string helping to determine if we should strip
485 ``AssertionError`` from the output. Defaults to the exception
486 message/``__str__()``.
487 """
488 _striptext = ""
489 if exprinfo is None and isinstance(exc_info[1], AssertionError):
490 exprinfo = getattr(exc_info[1], "msg", None)
491 if exprinfo is None:
492 exprinfo = saferepr(exc_info[1])
493 if exprinfo and exprinfo.startswith(cls._assert_start_repr):
494 _striptext = "AssertionError: "
496 return cls(exc_info, _striptext, _ispytest=True)
498 @classmethod
499 def from_current(
500 cls, exprinfo: Optional[str] = None
501 ) -> "ExceptionInfo[BaseException]":
502 """Return an ExceptionInfo matching the current traceback.
504 .. warning::
506 Experimental API
508 :param exprinfo:
509 A text string helping to determine if we should strip
510 ``AssertionError`` from the output. Defaults to the exception
511 message/``__str__()``.
512 """
513 tup = sys.exc_info()
514 assert tup[0] is not None, "no current exception"
515 assert tup[1] is not None, "no current exception"
516 assert tup[2] is not None, "no current exception"
517 exc_info = (tup[0], tup[1], tup[2])
518 return ExceptionInfo.from_exc_info(exc_info, exprinfo)
520 @classmethod
521 def for_later(cls) -> "ExceptionInfo[E]":
522 """Return an unfilled ExceptionInfo."""
523 return cls(None, _ispytest=True)
525 def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
526 """Fill an unfilled ExceptionInfo created with ``for_later()``."""
527 assert self._excinfo is None, "ExceptionInfo was already filled"
528 self._excinfo = exc_info
530 @property
531 def type(self) -> Type[E]:
532 """The exception class."""
533 assert (
534 self._excinfo is not None
535 ), ".type can only be used after the context manager exits"
536 return self._excinfo[0]
538 @property
539 def value(self) -> E:
540 """The exception value."""
541 assert (
542 self._excinfo is not None
543 ), ".value can only be used after the context manager exits"
544 return self._excinfo[1]
546 @property
547 def tb(self) -> TracebackType:
548 """The exception raw traceback."""
549 assert (
550 self._excinfo is not None
551 ), ".tb can only be used after the context manager exits"
552 return self._excinfo[2]
554 @property
555 def typename(self) -> str:
556 """The type name of the exception."""
557 assert (
558 self._excinfo is not None
559 ), ".typename can only be used after the context manager exits"
560 return self.type.__name__
562 @property
563 def traceback(self) -> Traceback:
564 """The traceback."""
565 if self._traceback is None:
566 self._traceback = Traceback(self.tb, excinfo=ref(self))
567 return self._traceback
569 @traceback.setter
570 def traceback(self, value: Traceback) -> None:
571 self._traceback = value
573 def __repr__(self) -> str:
574 if self._excinfo is None:
575 return "<ExceptionInfo for raises contextmanager>"
576 return "<{} {} tblen={}>".format(
577 self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
578 )
580 def exconly(self, tryshort: bool = False) -> str:
581 """Return the exception as a string.
583 When 'tryshort' resolves to True, and the exception is an
584 AssertionError, only the actual exception part of the exception
585 representation is returned (so 'AssertionError: ' is removed from
586 the beginning).
587 """
588 lines = format_exception_only(self.type, self.value)
589 text = "".join(lines)
590 text = text.rstrip()
591 if tryshort:
592 if text.startswith(self._striptext):
593 text = text[len(self._striptext) :]
594 return text
596 def errisinstance(
597 self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
598 ) -> bool:
599 """Return True if the exception is an instance of exc.
601 Consider using ``isinstance(excinfo.value, exc)`` instead.
602 """
603 return isinstance(self.value, exc)
605 def _getreprcrash(self) -> "ReprFileLocation":
606 exconly = self.exconly(tryshort=True)
607 entry = self.traceback.getcrashentry()
608 path, lineno = entry.frame.code.raw.co_filename, entry.lineno
609 return ReprFileLocation(path, lineno + 1, exconly)
611 def getrepr(
612 self,
613 showlocals: bool = False,
614 style: "_TracebackStyle" = "long",
615 abspath: bool = False,
616 tbfilter: bool = True,
617 funcargs: bool = False,
618 truncate_locals: bool = True,
619 chain: bool = True,
620 ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
621 """Return str()able representation of this exception info.
623 :param bool showlocals:
624 Show locals per traceback entry.
625 Ignored if ``style=="native"``.
627 :param str style:
628 long|short|no|native|value traceback style.
630 :param bool abspath:
631 If paths should be changed to absolute or left unchanged.
633 :param bool tbfilter:
634 Hide entries that contain a local variable ``__tracebackhide__==True``.
635 Ignored if ``style=="native"``.
637 :param bool funcargs:
638 Show fixtures ("funcargs" for legacy purposes) per traceback entry.
640 :param bool truncate_locals:
641 With ``showlocals==True``, make sure locals can be safely represented as strings.
643 :param bool chain:
644 If chained exceptions in Python 3 should be shown.
646 .. versionchanged:: 3.9
648 Added the ``chain`` parameter.
649 """
650 if style == "native":
651 return ReprExceptionInfo(
652 ReprTracebackNative(
653 traceback.format_exception(
654 self.type, self.value, self.traceback[0]._rawentry
655 )
656 ),
657 self._getreprcrash(),
658 )
660 fmt = FormattedExcinfo(
661 showlocals=showlocals,
662 style=style,
663 abspath=abspath,
664 tbfilter=tbfilter,
665 funcargs=funcargs,
666 truncate_locals=truncate_locals,
667 chain=chain,
668 )
669 return fmt.repr_excinfo(self)
671 def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
672 """Check whether the regular expression `regexp` matches the string
673 representation of the exception using :func:`python:re.search`.
675 If it matches `True` is returned, otherwise an `AssertionError` is raised.
676 """
677 __tracebackhide__ = True
678 value = str(self.value)
679 msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
680 if regexp == value:
681 msg += "\n Did you mean to `re.escape()` the regex?"
682 assert re.search(regexp, value), msg
683 # Return True to allow for "assert excinfo.match()".
684 return True
687@attr.s(auto_attribs=True)
688class FormattedExcinfo:
689 """Presenting information about failing Functions and Generators."""
691 # for traceback entries
692 flow_marker: ClassVar = ">"
693 fail_marker: ClassVar = "E"
695 showlocals: bool = False
696 style: "_TracebackStyle" = "long"
697 abspath: bool = True
698 tbfilter: bool = True
699 funcargs: bool = False
700 truncate_locals: bool = True
701 chain: bool = True
702 astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
703 factory=dict, init=False, repr=False
704 )
706 def _getindent(self, source: "Source") -> int:
707 # Figure out indent for the given source.
708 try:
709 s = str(source.getstatement(len(source) - 1))
710 except KeyboardInterrupt:
711 raise
712 except BaseException:
713 try:
714 s = str(source[-1])
715 except KeyboardInterrupt:
716 raise
717 except BaseException:
718 return 0
719 return 4 + (len(s) - len(s.lstrip()))
721 def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
722 source = entry.getsource(self.astcache)
723 if source is not None:
724 source = source.deindent()
725 return source
727 def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
728 if self.funcargs:
729 args = []
730 for argname, argvalue in entry.frame.getargs(var=True):
731 args.append((argname, saferepr(argvalue)))
732 return ReprFuncArgs(args)
733 return None
735 def get_source(
736 self,
737 source: Optional["Source"],
738 line_index: int = -1,
739 excinfo: Optional[ExceptionInfo[BaseException]] = None,
740 short: bool = False,
741 ) -> List[str]:
742 """Return formatted and marked up source lines."""
743 lines = []
744 if source is None or line_index >= len(source.lines):
745 source = Source("???")
746 line_index = 0
747 if line_index < 0:
748 line_index += len(source)
749 space_prefix = " "
750 if short:
751 lines.append(space_prefix + source.lines[line_index].strip())
752 else:
753 for line in source.lines[:line_index]:
754 lines.append(space_prefix + line)
755 lines.append(self.flow_marker + " " + source.lines[line_index])
756 for line in source.lines[line_index + 1 :]:
757 lines.append(space_prefix + line)
758 if excinfo is not None:
759 indent = 4 if short else self._getindent(source)
760 lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
761 return lines
763 def get_exconly(
764 self,
765 excinfo: ExceptionInfo[BaseException],
766 indent: int = 4,
767 markall: bool = False,
768 ) -> List[str]:
769 lines = []
770 indentstr = " " * indent
771 # Get the real exception information out.
772 exlines = excinfo.exconly(tryshort=True).split("\n")
773 failindent = self.fail_marker + indentstr[1:]
774 for line in exlines:
775 lines.append(failindent + line)
776 if not markall:
777 failindent = indentstr
778 return lines
780 def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
781 if self.showlocals:
782 lines = []
783 keys = [loc for loc in locals if loc[0] != "@"]
784 keys.sort()
785 for name in keys:
786 value = locals[name]
787 if name == "__builtins__":
788 lines.append("__builtins__ = <builtins>")
789 else:
790 # This formatting could all be handled by the
791 # _repr() function, which is only reprlib.Repr in
792 # disguise, so is very configurable.
793 if self.truncate_locals:
794 str_repr = saferepr(value)
795 else:
796 str_repr = safeformat(value)
797 # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
798 lines.append(f"{name:<10} = {str_repr}")
799 # else:
800 # self._line("%-10s =\\" % (name,))
801 # # XXX
802 # pprint.pprint(value, stream=self.excinfowriter)
803 return ReprLocals(lines)
804 return None
806 def repr_traceback_entry(
807 self,
808 entry: TracebackEntry,
809 excinfo: Optional[ExceptionInfo[BaseException]] = None,
810 ) -> "ReprEntry":
811 lines: List[str] = []
812 style = entry._repr_style if entry._repr_style is not None else self.style
813 if style in ("short", "long"):
814 source = self._getentrysource(entry)
815 if source is None:
816 source = Source("???")
817 line_index = 0
818 else:
819 line_index = entry.lineno - entry.getfirstlinesource()
820 short = style == "short"
821 reprargs = self.repr_args(entry) if not short else None
822 s = self.get_source(source, line_index, excinfo, short=short)
823 lines.extend(s)
824 if short:
825 message = "in %s" % (entry.name)
826 else:
827 message = excinfo and excinfo.typename or ""
828 entry_path = entry.path
829 path = self._makepath(entry_path)
830 reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
831 localsrepr = self.repr_locals(entry.locals)
832 return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
833 elif style == "value":
834 if excinfo:
835 lines.extend(str(excinfo.value).split("\n"))
836 return ReprEntry(lines, None, None, None, style)
837 else:
838 if excinfo:
839 lines.extend(self.get_exconly(excinfo, indent=4))
840 return ReprEntry(lines, None, None, None, style)
842 def _makepath(self, path: Union[Path, str]) -> str:
843 if not self.abspath and isinstance(path, Path):
844 try:
845 np = bestrelpath(Path.cwd(), path)
846 except OSError:
847 return str(path)
848 if len(np) < len(str(path)):
849 return np
850 return str(path)
852 def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
853 traceback = excinfo.traceback
854 if self.tbfilter:
855 traceback = traceback.filter()
857 if isinstance(excinfo.value, RecursionError):
858 traceback, extraline = self._truncate_recursive_traceback(traceback)
859 else:
860 extraline = None
862 last = traceback[-1]
863 entries = []
864 if self.style == "value":
865 reprentry = self.repr_traceback_entry(last, excinfo)
866 entries.append(reprentry)
867 return ReprTraceback(entries, None, style=self.style)
869 for index, entry in enumerate(traceback):
870 einfo = (last == entry) and excinfo or None
871 reprentry = self.repr_traceback_entry(entry, einfo)
872 entries.append(reprentry)
873 return ReprTraceback(entries, extraline, style=self.style)
875 def _truncate_recursive_traceback(
876 self, traceback: Traceback
877 ) -> Tuple[Traceback, Optional[str]]:
878 """Truncate the given recursive traceback trying to find the starting
879 point of the recursion.
881 The detection is done by going through each traceback entry and
882 finding the point in which the locals of the frame are equal to the
883 locals of a previous frame (see ``recursionindex()``).
885 Handle the situation where the recursion process might raise an
886 exception (for example comparing numpy arrays using equality raises a
887 TypeError), in which case we do our best to warn the user of the
888 error and show a limited traceback.
889 """
890 try:
891 recursionindex = traceback.recursionindex()
892 except Exception as e:
893 max_frames = 10
894 extraline: Optional[str] = (
895 "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
896 " The following exception happened when comparing locals in the stack frame:\n"
897 " {exc_type}: {exc_msg}\n"
898 " Displaying first and last {max_frames} stack frames out of {total}."
899 ).format(
900 exc_type=type(e).__name__,
901 exc_msg=str(e),
902 max_frames=max_frames,
903 total=len(traceback),
904 )
905 # Type ignored because adding two instances of a List subtype
906 # currently incorrectly has type List instead of the subtype.
907 traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
908 else:
909 if recursionindex is not None:
910 extraline = "!!! Recursion detected (same locals & position)"
911 traceback = traceback[: recursionindex + 1]
912 else:
913 extraline = None
915 return traceback, extraline
917 def repr_excinfo(
918 self, excinfo: ExceptionInfo[BaseException]
919 ) -> "ExceptionChainRepr":
920 repr_chain: List[
921 Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
922 ] = []
923 e: Optional[BaseException] = excinfo.value
924 excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
925 descr = None
926 seen: Set[int] = set()
927 while e is not None and id(e) not in seen:
928 seen.add(id(e))
929 if excinfo_:
930 # Fall back to native traceback as a temporary workaround until
931 # full support for exception groups added to ExceptionInfo.
932 # See https://github.com/pytest-dev/pytest/issues/9159
933 if isinstance(e, BaseExceptionGroup):
934 reprtraceback: Union[
935 ReprTracebackNative, ReprTraceback
936 ] = ReprTracebackNative(
937 traceback.format_exception(
938 type(excinfo_.value),
939 excinfo_.value,
940 excinfo_.traceback[0]._rawentry,
941 )
942 )
943 else:
944 reprtraceback = self.repr_traceback(excinfo_)
945 reprcrash: Optional[ReprFileLocation] = (
946 excinfo_._getreprcrash() if self.style != "value" else None
947 )
948 else:
949 # Fallback to native repr if the exception doesn't have a traceback:
950 # ExceptionInfo objects require a full traceback to work.
951 reprtraceback = ReprTracebackNative(
952 traceback.format_exception(type(e), e, None)
953 )
954 reprcrash = None
956 repr_chain += [(reprtraceback, reprcrash, descr)]
957 if e.__cause__ is not None and self.chain:
958 e = e.__cause__
959 excinfo_ = (
960 ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
961 if e.__traceback__
962 else None
963 )
964 descr = "The above exception was the direct cause of the following exception:"
965 elif (
966 e.__context__ is not None and not e.__suppress_context__ and self.chain
967 ):
968 e = e.__context__
969 excinfo_ = (
970 ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
971 if e.__traceback__
972 else None
973 )
974 descr = "During handling of the above exception, another exception occurred:"
975 else:
976 e = None
977 repr_chain.reverse()
978 return ExceptionChainRepr(repr_chain)
981@attr.s(eq=False, auto_attribs=True)
982class TerminalRepr:
983 def __str__(self) -> str:
984 # FYI this is called from pytest-xdist's serialization of exception
985 # information.
986 io = StringIO()
987 tw = TerminalWriter(file=io)
988 self.toterminal(tw)
989 return io.getvalue().strip()
991 def __repr__(self) -> str:
992 return f"<{self.__class__} instance at {id(self):0x}>"
994 def toterminal(self, tw: TerminalWriter) -> None:
995 raise NotImplementedError()
998# This class is abstract -- only subclasses are instantiated.
999@attr.s(eq=False)
1000class ExceptionRepr(TerminalRepr):
1001 # Provided by subclasses.
1002 reprcrash: Optional["ReprFileLocation"]
1003 reprtraceback: "ReprTraceback"
1005 def __attrs_post_init__(self) -> None:
1006 self.sections: List[Tuple[str, str, str]] = []
1008 def addsection(self, name: str, content: str, sep: str = "-") -> None:
1009 self.sections.append((name, content, sep))
1011 def toterminal(self, tw: TerminalWriter) -> None:
1012 for name, content, sep in self.sections:
1013 tw.sep(sep, name)
1014 tw.line(content)
1017@attr.s(eq=False, auto_attribs=True)
1018class ExceptionChainRepr(ExceptionRepr):
1019 chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
1021 def __attrs_post_init__(self) -> None:
1022 super().__attrs_post_init__()
1023 # reprcrash and reprtraceback of the outermost (the newest) exception
1024 # in the chain.
1025 self.reprtraceback = self.chain[-1][0]
1026 self.reprcrash = self.chain[-1][1]
1028 def toterminal(self, tw: TerminalWriter) -> None:
1029 for element in self.chain:
1030 element[0].toterminal(tw)
1031 if element[2] is not None:
1032 tw.line("")
1033 tw.line(element[2], yellow=True)
1034 super().toterminal(tw)
1037@attr.s(eq=False, auto_attribs=True)
1038class ReprExceptionInfo(ExceptionRepr):
1039 reprtraceback: "ReprTraceback"
1040 reprcrash: "ReprFileLocation"
1042 def toterminal(self, tw: TerminalWriter) -> None:
1043 self.reprtraceback.toterminal(tw)
1044 super().toterminal(tw)
1047@attr.s(eq=False, auto_attribs=True)
1048class ReprTraceback(TerminalRepr):
1049 reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
1050 extraline: Optional[str]
1051 style: "_TracebackStyle"
1053 entrysep: ClassVar = "_ "
1055 def toterminal(self, tw: TerminalWriter) -> None:
1056 # The entries might have different styles.
1057 for i, entry in enumerate(self.reprentries):
1058 if entry.style == "long":
1059 tw.line("")
1060 entry.toterminal(tw)
1061 if i < len(self.reprentries) - 1:
1062 next_entry = self.reprentries[i + 1]
1063 if (
1064 entry.style == "long"
1065 or entry.style == "short"
1066 and next_entry.style == "long"
1067 ):
1068 tw.sep(self.entrysep)
1070 if self.extraline:
1071 tw.line(self.extraline)
1074class ReprTracebackNative(ReprTraceback):
1075 def __init__(self, tblines: Sequence[str]) -> None:
1076 self.style = "native"
1077 self.reprentries = [ReprEntryNative(tblines)]
1078 self.extraline = None
1081@attr.s(eq=False, auto_attribs=True)
1082class ReprEntryNative(TerminalRepr):
1083 lines: Sequence[str]
1085 style: ClassVar["_TracebackStyle"] = "native"
1087 def toterminal(self, tw: TerminalWriter) -> None:
1088 tw.write("".join(self.lines))
1091@attr.s(eq=False, auto_attribs=True)
1092class ReprEntry(TerminalRepr):
1093 lines: Sequence[str]
1094 reprfuncargs: Optional["ReprFuncArgs"]
1095 reprlocals: Optional["ReprLocals"]
1096 reprfileloc: Optional["ReprFileLocation"]
1097 style: "_TracebackStyle"
1099 def _write_entry_lines(self, tw: TerminalWriter) -> None:
1100 """Write the source code portions of a list of traceback entries with syntax highlighting.
1102 Usually entries are lines like these:
1104 " x = 1"
1105 "> assert x == 2"
1106 "E assert 1 == 2"
1108 This function takes care of rendering the "source" portions of it (the lines without
1109 the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
1110 character, as doing so might break line continuations.
1111 """
1113 if not self.lines:
1114 return
1116 # separate indents and source lines that are not failures: we want to
1117 # highlight the code but not the indentation, which may contain markers
1118 # such as "> assert 0"
1119 fail_marker = f"{FormattedExcinfo.fail_marker} "
1120 indent_size = len(fail_marker)
1121 indents: List[str] = []
1122 source_lines: List[str] = []
1123 failure_lines: List[str] = []
1124 for index, line in enumerate(self.lines):
1125 is_failure_line = line.startswith(fail_marker)
1126 if is_failure_line:
1127 # from this point on all lines are considered part of the failure
1128 failure_lines.extend(self.lines[index:])
1129 break
1130 else:
1131 if self.style == "value":
1132 source_lines.append(line)
1133 else:
1134 indents.append(line[:indent_size])
1135 source_lines.append(line[indent_size:])
1137 tw._write_source(source_lines, indents)
1139 # failure lines are always completely red and bold
1140 for line in failure_lines:
1141 tw.line(line, bold=True, red=True)
1143 def toterminal(self, tw: TerminalWriter) -> None:
1144 if self.style == "short":
1145 assert self.reprfileloc is not None
1146 self.reprfileloc.toterminal(tw)
1147 self._write_entry_lines(tw)
1148 if self.reprlocals:
1149 self.reprlocals.toterminal(tw, indent=" " * 8)
1150 return
1152 if self.reprfuncargs:
1153 self.reprfuncargs.toterminal(tw)
1155 self._write_entry_lines(tw)
1157 if self.reprlocals:
1158 tw.line("")
1159 self.reprlocals.toterminal(tw)
1160 if self.reprfileloc:
1161 if self.lines:
1162 tw.line("")
1163 self.reprfileloc.toterminal(tw)
1165 def __str__(self) -> str:
1166 return "{}\n{}\n{}".format(
1167 "\n".join(self.lines), self.reprlocals, self.reprfileloc
1168 )
1171@attr.s(eq=False, auto_attribs=True)
1172class ReprFileLocation(TerminalRepr):
1173 path: str = attr.ib(converter=str)
1174 lineno: int
1175 message: str
1177 def toterminal(self, tw: TerminalWriter) -> None:
1178 # Filename and lineno output for each entry, using an output format
1179 # that most editors understand.
1180 msg = self.message
1181 i = msg.find("\n")
1182 if i != -1:
1183 msg = msg[:i]
1184 tw.write(self.path, bold=True, red=True)
1185 tw.line(f":{self.lineno}: {msg}")
1188@attr.s(eq=False, auto_attribs=True)
1189class ReprLocals(TerminalRepr):
1190 lines: Sequence[str]
1192 def toterminal(self, tw: TerminalWriter, indent="") -> None:
1193 for line in self.lines:
1194 tw.line(indent + line)
1197@attr.s(eq=False, auto_attribs=True)
1198class ReprFuncArgs(TerminalRepr):
1199 args: Sequence[Tuple[str, object]]
1201 def toterminal(self, tw: TerminalWriter) -> None:
1202 if self.args:
1203 linesofar = ""
1204 for name, value in self.args:
1205 ns = f"{name} = {value}"
1206 if len(ns) + len(linesofar) + 2 > tw.fullwidth:
1207 if linesofar:
1208 tw.line(linesofar)
1209 linesofar = ns
1210 else:
1211 if linesofar:
1212 linesofar += ", " + ns
1213 else:
1214 linesofar = ns
1215 if linesofar:
1216 tw.line(linesofar)
1217 tw.line("")
1220def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
1221 """Return source location (path, lineno) for the given object.
1223 If the source cannot be determined return ("", -1).
1225 The line number is 0-based.
1226 """
1227 # xxx let decorators etc specify a sane ordering
1228 # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
1229 # in 6ec13a2b9. It ("place_as") appears to be something very custom.
1230 obj = get_real_func(obj)
1231 if hasattr(obj, "place_as"):
1232 obj = obj.place_as # type: ignore[attr-defined]
1234 try:
1235 code = Code.from_function(obj)
1236 except TypeError:
1237 try:
1238 fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
1239 except TypeError:
1240 return "", -1
1242 fspath = fn and absolutepath(fn) or ""
1243 lineno = -1
1244 if fspath:
1245 try:
1246 _, lineno = findsource(obj)
1247 except OSError:
1248 pass
1249 return fspath, lineno
1251 return code.path, code.firstlineno
1254# Relative paths that we use to filter traceback entries from appearing to the user;
1255# see filter_traceback.
1256# note: if we need to add more paths than what we have now we should probably use a list
1257# for better maintenance.
1259_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
1260# pluggy is either a package or a single module depending on the version
1261if _PLUGGY_DIR.name == "__init__.py":
1262 _PLUGGY_DIR = _PLUGGY_DIR.parent
1263_PYTEST_DIR = Path(_pytest.__file__).parent
1266def filter_traceback(entry: TracebackEntry) -> bool:
1267 """Return True if a TracebackEntry instance should be included in tracebacks.
1269 We hide traceback entries of:
1271 * dynamically generated code (no code to show up for it);
1272 * internal traceback from pytest or its internal libraries, py and pluggy.
1273 """
1274 # entry.path might sometimes return a str object when the entry
1275 # points to dynamically generated code.
1276 # See https://bitbucket.org/pytest-dev/py/issues/71.
1277 raw_filename = entry.frame.code.raw.co_filename
1278 is_generated = "<" in raw_filename and ">" in raw_filename
1279 if is_generated:
1280 return False
1282 # entry.path might point to a non-existing file, in which case it will
1283 # also return a str object. See #1133.
1284 p = Path(entry.path)
1286 parents = p.parents
1287 if _PLUGGY_DIR in parents:
1288 return False
1289 if _PYTEST_DIR in parents:
1290 return False
1292 return True