12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337 |
- import ast
- import dataclasses
- import inspect
- import os
- import re
- import sys
- import traceback
- from inspect import CO_VARARGS
- from inspect import CO_VARKEYWORDS
- from io import StringIO
- from pathlib import Path
- from traceback import format_exception_only
- from types import CodeType
- from types import FrameType
- from types import TracebackType
- from typing import Any
- from typing import Callable
- from typing import ClassVar
- from typing import Dict
- from typing import Generic
- from typing import Iterable
- from typing import List
- from typing import Mapping
- from typing import Optional
- from typing import overload
- from typing import Pattern
- from typing import Sequence
- from typing import Set
- from typing import Tuple
- from typing import Type
- from typing import TYPE_CHECKING
- from typing import TypeVar
- from typing import Union
- import pluggy
- import _pytest
- from _pytest._code.source import findsource
- from _pytest._code.source import getrawcode
- from _pytest._code.source import getstatementrange_ast
- from _pytest._code.source import Source
- from _pytest._io import TerminalWriter
- from _pytest._io.saferepr import safeformat
- from _pytest._io.saferepr import saferepr
- from _pytest.compat import final
- from _pytest.compat import get_real_func
- from _pytest.deprecated import check_ispytest
- from _pytest.pathlib import absolutepath
- from _pytest.pathlib import bestrelpath
- if TYPE_CHECKING:
- from typing_extensions import Final
- from typing_extensions import Literal
- from typing_extensions import SupportsIndex
- _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
- if sys.version_info[:2] < (3, 11):
- from exceptiongroup import BaseExceptionGroup
- class Code:
- """Wrapper around Python code objects."""
- __slots__ = ("raw",)
- def __init__(self, obj: CodeType) -> None:
- self.raw = obj
- @classmethod
- def from_function(cls, obj: object) -> "Code":
- return cls(getrawcode(obj))
- def __eq__(self, other):
- return self.raw == other.raw
- # Ignore type because of https://github.com/python/mypy/issues/4266.
- __hash__ = None # type: ignore
- @property
- def firstlineno(self) -> int:
- return self.raw.co_firstlineno - 1
- @property
- def name(self) -> str:
- return self.raw.co_name
- @property
- def path(self) -> Union[Path, str]:
- """Return a path object pointing to source code, or an ``str`` in
- case of ``OSError`` / non-existing file."""
- if not self.raw.co_filename:
- return ""
- try:
- p = absolutepath(self.raw.co_filename)
- # maybe don't try this checking
- if not p.exists():
- raise OSError("path check failed.")
- return p
- except OSError:
- # XXX maybe try harder like the weird logic
- # in the standard lib [linecache.updatecache] does?
- return self.raw.co_filename
- @property
- def fullsource(self) -> Optional["Source"]:
- """Return a _pytest._code.Source object for the full source file of the code."""
- full, _ = findsource(self.raw)
- return full
- def source(self) -> "Source":
- """Return a _pytest._code.Source object for the code object's source only."""
- # return source only for that part of code
- return Source(self.raw)
- def getargs(self, var: bool = False) -> Tuple[str, ...]:
- """Return a tuple with the argument names for the code object.
- If 'var' is set True also return the names of the variable and
- keyword arguments when present.
- """
- # Handy shortcut for getting args.
- raw = self.raw
- argcount = raw.co_argcount
- if var:
- argcount += raw.co_flags & CO_VARARGS
- argcount += raw.co_flags & CO_VARKEYWORDS
- return raw.co_varnames[:argcount]
- class Frame:
- """Wrapper around a Python frame holding f_locals and f_globals
- in which expressions can be evaluated."""
- __slots__ = ("raw",)
- def __init__(self, frame: FrameType) -> None:
- self.raw = frame
- @property
- def lineno(self) -> int:
- return self.raw.f_lineno - 1
- @property
- def f_globals(self) -> Dict[str, Any]:
- return self.raw.f_globals
- @property
- def f_locals(self) -> Dict[str, Any]:
- return self.raw.f_locals
- @property
- def code(self) -> Code:
- return Code(self.raw.f_code)
- @property
- def statement(self) -> "Source":
- """Statement this frame is at."""
- if self.code.fullsource is None:
- return Source("")
- return self.code.fullsource.getstatement(self.lineno)
- def eval(self, code, **vars):
- """Evaluate 'code' in the frame.
- 'vars' are optional additional local variables.
- Returns the result of the evaluation.
- """
- f_locals = self.f_locals.copy()
- f_locals.update(vars)
- return eval(code, self.f_globals, f_locals)
- def repr(self, object: object) -> str:
- """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
- return saferepr(object)
- def getargs(self, var: bool = False):
- """Return a list of tuples (name, value) for all arguments.
- If 'var' is set True, also include the variable and keyword arguments
- when present.
- """
- retval = []
- for arg in self.code.getargs(var):
- try:
- retval.append((arg, self.f_locals[arg]))
- except KeyError:
- pass # this can occur when using Psyco
- return retval
- class TracebackEntry:
- """A single entry in a Traceback."""
- __slots__ = ("_rawentry", "_repr_style")
- def __init__(
- self,
- rawentry: TracebackType,
- repr_style: Optional['Literal["short", "long"]'] = None,
- ) -> None:
- self._rawentry: "Final" = rawentry
- self._repr_style: "Final" = repr_style
- def with_repr_style(
- self, repr_style: Optional['Literal["short", "long"]']
- ) -> "TracebackEntry":
- return TracebackEntry(self._rawentry, repr_style)
- @property
- def lineno(self) -> int:
- return self._rawentry.tb_lineno - 1
- @property
- def frame(self) -> Frame:
- return Frame(self._rawentry.tb_frame)
- @property
- def relline(self) -> int:
- return self.lineno - self.frame.code.firstlineno
- def __repr__(self) -> str:
- return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
- @property
- def statement(self) -> "Source":
- """_pytest._code.Source object for the current statement."""
- source = self.frame.code.fullsource
- assert source is not None
- return source.getstatement(self.lineno)
- @property
- def path(self) -> Union[Path, str]:
- """Path to the source code."""
- return self.frame.code.path
- @property
- def locals(self) -> Dict[str, Any]:
- """Locals of underlying frame."""
- return self.frame.f_locals
- def getfirstlinesource(self) -> int:
- return self.frame.code.firstlineno
- def getsource(
- self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None
- ) -> Optional["Source"]:
- """Return failing source code."""
- # we use the passed in astcache to not reparse asttrees
- # within exception info printing
- source = self.frame.code.fullsource
- if source is None:
- return None
- key = astnode = None
- if astcache is not None:
- key = self.frame.code.path
- if key is not None:
- astnode = astcache.get(key, None)
- start = self.getfirstlinesource()
- try:
- astnode, _, end = getstatementrange_ast(
- self.lineno, source, astnode=astnode
- )
- except SyntaxError:
- end = self.lineno + 1
- else:
- if key is not None and astcache is not None:
- astcache[key] = astnode
- return source[start:end]
- source = property(getsource)
- def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
- """Return True if the current frame has a var __tracebackhide__
- resolving to True.
- If __tracebackhide__ is a callable, it gets called with the
- ExceptionInfo instance and can decide whether to hide the traceback.
- Mostly for internal use.
- """
- tbh: Union[
- bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]
- ] = False
- for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
- # in normal cases, f_locals and f_globals are dictionaries
- # however via `exec(...)` / `eval(...)` they can be other types
- # (even incorrect types!).
- # as such, we suppress all exceptions while accessing __tracebackhide__
- try:
- tbh = maybe_ns_dct["__tracebackhide__"]
- except Exception:
- pass
- else:
- break
- if tbh and callable(tbh):
- return tbh(excinfo)
- return tbh
- def __str__(self) -> str:
- name = self.frame.code.name
- try:
- line = str(self.statement).lstrip()
- except KeyboardInterrupt:
- raise
- except BaseException:
- line = "???"
- # This output does not quite match Python's repr for traceback entries,
- # but changing it to do so would break certain plugins. See
- # https://github.com/pytest-dev/pytest/pull/7535/ for details.
- return " File %r:%d in %s\n %s\n" % (
- str(self.path),
- self.lineno + 1,
- name,
- line,
- )
- @property
- def name(self) -> str:
- """co_name of underlying code."""
- return self.frame.code.raw.co_name
- class Traceback(List[TracebackEntry]):
- """Traceback objects encapsulate and offer higher level access to Traceback entries."""
- def __init__(
- self,
- tb: Union[TracebackType, Iterable[TracebackEntry]],
- ) -> None:
- """Initialize from given python traceback object and ExceptionInfo."""
- if isinstance(tb, TracebackType):
- def f(cur: TracebackType) -> Iterable[TracebackEntry]:
- cur_: Optional[TracebackType] = cur
- while cur_ is not None:
- yield TracebackEntry(cur_)
- cur_ = cur_.tb_next
- super().__init__(f(tb))
- else:
- super().__init__(tb)
- def cut(
- self,
- path: Optional[Union["os.PathLike[str]", str]] = None,
- lineno: Optional[int] = None,
- firstlineno: Optional[int] = None,
- excludepath: Optional["os.PathLike[str]"] = None,
- ) -> "Traceback":
- """Return a Traceback instance wrapping part of this Traceback.
- By providing any combination of path, lineno and firstlineno, the
- first frame to start the to-be-returned traceback is determined.
- This allows cutting the first part of a Traceback instance e.g.
- for formatting reasons (removing some uninteresting bits that deal
- with handling of the exception/traceback).
- """
- path_ = None if path is None else os.fspath(path)
- excludepath_ = None if excludepath is None else os.fspath(excludepath)
- for x in self:
- code = x.frame.code
- codepath = code.path
- if path is not None and str(codepath) != path_:
- continue
- if (
- excludepath is not None
- and isinstance(codepath, Path)
- and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
- ):
- continue
- if lineno is not None and x.lineno != lineno:
- continue
- if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
- continue
- return Traceback(x._rawentry)
- return self
- @overload
- def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
- ...
- @overload
- def __getitem__(self, key: slice) -> "Traceback":
- ...
- def __getitem__(
- self, key: Union["SupportsIndex", slice]
- ) -> Union[TracebackEntry, "Traceback"]:
- if isinstance(key, slice):
- return self.__class__(super().__getitem__(key))
- else:
- return super().__getitem__(key)
- def filter(
- self,
- # TODO(py38): change to positional only.
- _excinfo_or_fn: Union[
- "ExceptionInfo[BaseException]",
- Callable[[TracebackEntry], bool],
- ],
- ) -> "Traceback":
- """Return a Traceback instance with certain items removed.
- If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
- which are hidden (see ishidden() above).
- Otherwise, the filter is a function that gets a single argument, a
- ``TracebackEntry`` instance, and should return True when the item should
- be added to the ``Traceback``, False when not.
- """
- if isinstance(_excinfo_or_fn, ExceptionInfo):
- fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
- else:
- fn = _excinfo_or_fn
- return Traceback(filter(fn, self))
- def recursionindex(self) -> Optional[int]:
- """Return the index of the frame/TracebackEntry where recursion originates if
- appropriate, None if no recursion occurred."""
- cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}
- for i, entry in enumerate(self):
- # id for the code.raw is needed to work around
- # the strange metaprogramming in the decorator lib from pypi
- # which generates code objects that have hash/value equality
- # XXX needs a test
- key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
- # print "checking for recursion at", key
- values = cache.setdefault(key, [])
- if values:
- f = entry.frame
- loc = f.f_locals
- for otherloc in values:
- if otherloc == loc:
- return i
- values.append(entry.frame.f_locals)
- return None
- E = TypeVar("E", bound=BaseException, covariant=True)
- @final
- @dataclasses.dataclass
- class ExceptionInfo(Generic[E]):
- """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
- _assert_start_repr: ClassVar = "AssertionError('assert "
- _excinfo: Optional[Tuple[Type["E"], "E", TracebackType]]
- _striptext: str
- _traceback: Optional[Traceback]
- def __init__(
- self,
- excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
- striptext: str = "",
- traceback: Optional[Traceback] = None,
- *,
- _ispytest: bool = False,
- ) -> None:
- check_ispytest(_ispytest)
- self._excinfo = excinfo
- self._striptext = striptext
- self._traceback = traceback
- @classmethod
- def from_exception(
- cls,
- # Ignoring error: "Cannot use a covariant type variable as a parameter".
- # This is OK to ignore because this class is (conceptually) readonly.
- # See https://github.com/python/mypy/issues/7049.
- exception: E, # type: ignore[misc]
- exprinfo: Optional[str] = None,
- ) -> "ExceptionInfo[E]":
- """Return an ExceptionInfo for an existing exception.
- The exception must have a non-``None`` ``__traceback__`` attribute,
- otherwise this function fails with an assertion error. This means that
- the exception must have been raised, or added a traceback with the
- :py:meth:`~BaseException.with_traceback()` method.
- :param exprinfo:
- A text string helping to determine if we should strip
- ``AssertionError`` from the output. Defaults to the exception
- message/``__str__()``.
- .. versionadded:: 7.4
- """
- assert (
- exception.__traceback__
- ), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
- exc_info = (type(exception), exception, exception.__traceback__)
- return cls.from_exc_info(exc_info, exprinfo)
- @classmethod
- def from_exc_info(
- cls,
- exc_info: Tuple[Type[E], E, TracebackType],
- exprinfo: Optional[str] = None,
- ) -> "ExceptionInfo[E]":
- """Like :func:`from_exception`, but using old-style exc_info tuple."""
- _striptext = ""
- if exprinfo is None and isinstance(exc_info[1], AssertionError):
- exprinfo = getattr(exc_info[1], "msg", None)
- if exprinfo is None:
- exprinfo = saferepr(exc_info[1])
- if exprinfo and exprinfo.startswith(cls._assert_start_repr):
- _striptext = "AssertionError: "
- return cls(exc_info, _striptext, _ispytest=True)
- @classmethod
- def from_current(
- cls, exprinfo: Optional[str] = None
- ) -> "ExceptionInfo[BaseException]":
- """Return an ExceptionInfo matching the current traceback.
- .. warning::
- Experimental API
- :param exprinfo:
- A text string helping to determine if we should strip
- ``AssertionError`` from the output. Defaults to the exception
- message/``__str__()``.
- """
- tup = sys.exc_info()
- assert tup[0] is not None, "no current exception"
- assert tup[1] is not None, "no current exception"
- assert tup[2] is not None, "no current exception"
- exc_info = (tup[0], tup[1], tup[2])
- return ExceptionInfo.from_exc_info(exc_info, exprinfo)
- @classmethod
- def for_later(cls) -> "ExceptionInfo[E]":
- """Return an unfilled ExceptionInfo."""
- return cls(None, _ispytest=True)
- def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
- """Fill an unfilled ExceptionInfo created with ``for_later()``."""
- assert self._excinfo is None, "ExceptionInfo was already filled"
- self._excinfo = exc_info
- @property
- def type(self) -> Type[E]:
- """The exception class."""
- assert (
- self._excinfo is not None
- ), ".type can only be used after the context manager exits"
- return self._excinfo[0]
- @property
- def value(self) -> E:
- """The exception value."""
- assert (
- self._excinfo is not None
- ), ".value can only be used after the context manager exits"
- return self._excinfo[1]
- @property
- def tb(self) -> TracebackType:
- """The exception raw traceback."""
- assert (
- self._excinfo is not None
- ), ".tb can only be used after the context manager exits"
- return self._excinfo[2]
- @property
- def typename(self) -> str:
- """The type name of the exception."""
- assert (
- self._excinfo is not None
- ), ".typename can only be used after the context manager exits"
- return self.type.__name__
- @property
- def traceback(self) -> Traceback:
- """The traceback."""
- if self._traceback is None:
- self._traceback = Traceback(self.tb)
- return self._traceback
- @traceback.setter
- def traceback(self, value: Traceback) -> None:
- self._traceback = value
- def __repr__(self) -> str:
- if self._excinfo is None:
- return "<ExceptionInfo for raises contextmanager>"
- return "<{} {} tblen={}>".format(
- self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
- )
- def exconly(self, tryshort: bool = False) -> str:
- """Return the exception as a string.
- When 'tryshort' resolves to True, and the exception is an
- AssertionError, only the actual exception part of the exception
- representation is returned (so 'AssertionError: ' is removed from
- the beginning).
- """
- lines = format_exception_only(self.type, self.value)
- text = "".join(lines)
- text = text.rstrip()
- if tryshort:
- if text.startswith(self._striptext):
- text = text[len(self._striptext) :]
- return text
- def errisinstance(
- self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
- ) -> bool:
- """Return True if the exception is an instance of exc.
- Consider using ``isinstance(excinfo.value, exc)`` instead.
- """
- return isinstance(self.value, exc)
- def _getreprcrash(self) -> Optional["ReprFileLocation"]:
- # Find last non-hidden traceback entry that led to the exception of the
- # traceback, or None if all hidden.
- for i in range(-1, -len(self.traceback) - 1, -1):
- entry = self.traceback[i]
- if not entry.ishidden(self):
- path, lineno = entry.frame.code.raw.co_filename, entry.lineno
- exconly = self.exconly(tryshort=True)
- return ReprFileLocation(path, lineno + 1, exconly)
- return None
- def getrepr(
- self,
- showlocals: bool = False,
- style: "_TracebackStyle" = "long",
- abspath: bool = False,
- tbfilter: Union[
- bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
- ] = True,
- funcargs: bool = False,
- truncate_locals: bool = True,
- chain: bool = True,
- ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
- """Return str()able representation of this exception info.
- :param bool showlocals:
- Show locals per traceback entry.
- Ignored if ``style=="native"``.
- :param str style:
- long|short|line|no|native|value traceback style.
- :param bool abspath:
- If paths should be changed to absolute or left unchanged.
- :param tbfilter:
- A filter for traceback entries.
- * If false, don't hide any entries.
- * If true, hide internal entries and entries that contain a local
- variable ``__tracebackhide__ = True``.
- * If a callable, delegates the filtering to the callable.
- Ignored if ``style`` is ``"native"``.
- :param bool funcargs:
- Show fixtures ("funcargs" for legacy purposes) per traceback entry.
- :param bool truncate_locals:
- With ``showlocals==True``, make sure locals can be safely represented as strings.
- :param bool chain:
- If chained exceptions in Python 3 should be shown.
- .. versionchanged:: 3.9
- Added the ``chain`` parameter.
- """
- if style == "native":
- return ReprExceptionInfo(
- reprtraceback=ReprTracebackNative(
- traceback.format_exception(
- self.type,
- self.value,
- self.traceback[0]._rawentry if self.traceback else None,
- )
- ),
- reprcrash=self._getreprcrash(),
- )
- fmt = FormattedExcinfo(
- showlocals=showlocals,
- style=style,
- abspath=abspath,
- tbfilter=tbfilter,
- funcargs=funcargs,
- truncate_locals=truncate_locals,
- chain=chain,
- )
- return fmt.repr_excinfo(self)
- def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
- """Check whether the regular expression `regexp` matches the string
- representation of the exception using :func:`python:re.search`.
- If it matches `True` is returned, otherwise an `AssertionError` is raised.
- """
- __tracebackhide__ = True
- value = str(self.value)
- msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
- if regexp == value:
- msg += "\n Did you mean to `re.escape()` the regex?"
- assert re.search(regexp, value), msg
- # Return True to allow for "assert excinfo.match()".
- return True
- @dataclasses.dataclass
- class FormattedExcinfo:
- """Presenting information about failing Functions and Generators."""
- # for traceback entries
- flow_marker: ClassVar = ">"
- fail_marker: ClassVar = "E"
- showlocals: bool = False
- style: "_TracebackStyle" = "long"
- abspath: bool = True
- tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
- funcargs: bool = False
- truncate_locals: bool = True
- chain: bool = True
- astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
- default_factory=dict, init=False, repr=False
- )
- def _getindent(self, source: "Source") -> int:
- # Figure out indent for the given source.
- try:
- s = str(source.getstatement(len(source) - 1))
- except KeyboardInterrupt:
- raise
- except BaseException:
- try:
- s = str(source[-1])
- except KeyboardInterrupt:
- raise
- except BaseException:
- return 0
- return 4 + (len(s) - len(s.lstrip()))
- def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
- source = entry.getsource(self.astcache)
- if source is not None:
- source = source.deindent()
- return source
- def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
- if self.funcargs:
- args = []
- for argname, argvalue in entry.frame.getargs(var=True):
- args.append((argname, saferepr(argvalue)))
- return ReprFuncArgs(args)
- return None
- def get_source(
- self,
- source: Optional["Source"],
- line_index: int = -1,
- excinfo: Optional[ExceptionInfo[BaseException]] = None,
- short: bool = False,
- ) -> List[str]:
- """Return formatted and marked up source lines."""
- lines = []
- if source is not None and line_index < 0:
- line_index += len(source)
- if source is None or line_index >= len(source.lines) or line_index < 0:
- # `line_index` could still be outside `range(len(source.lines))` if
- # we're processing AST with pathological position attributes.
- source = Source("???")
- line_index = 0
- space_prefix = " "
- if short:
- lines.append(space_prefix + source.lines[line_index].strip())
- else:
- for line in source.lines[:line_index]:
- lines.append(space_prefix + line)
- lines.append(self.flow_marker + " " + source.lines[line_index])
- for line in source.lines[line_index + 1 :]:
- lines.append(space_prefix + line)
- if excinfo is not None:
- indent = 4 if short else self._getindent(source)
- lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
- return lines
- def get_exconly(
- self,
- excinfo: ExceptionInfo[BaseException],
- indent: int = 4,
- markall: bool = False,
- ) -> List[str]:
- lines = []
- indentstr = " " * indent
- # Get the real exception information out.
- exlines = excinfo.exconly(tryshort=True).split("\n")
- failindent = self.fail_marker + indentstr[1:]
- for line in exlines:
- lines.append(failindent + line)
- if not markall:
- failindent = indentstr
- return lines
- def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
- if self.showlocals:
- lines = []
- keys = [loc for loc in locals if loc[0] != "@"]
- keys.sort()
- for name in keys:
- value = locals[name]
- if name == "__builtins__":
- lines.append("__builtins__ = <builtins>")
- else:
- # This formatting could all be handled by the
- # _repr() function, which is only reprlib.Repr in
- # disguise, so is very configurable.
- if self.truncate_locals:
- str_repr = saferepr(value)
- else:
- str_repr = safeformat(value)
- # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
- lines.append(f"{name:<10} = {str_repr}")
- # else:
- # self._line("%-10s =\\" % (name,))
- # # XXX
- # pprint.pprint(value, stream=self.excinfowriter)
- return ReprLocals(lines)
- return None
- def repr_traceback_entry(
- self,
- entry: Optional[TracebackEntry],
- excinfo: Optional[ExceptionInfo[BaseException]] = None,
- ) -> "ReprEntry":
- lines: List[str] = []
- style = (
- entry._repr_style
- if entry is not None and entry._repr_style is not None
- else self.style
- )
- if style in ("short", "long") and entry is not None:
- source = self._getentrysource(entry)
- if source is None:
- source = Source("???")
- line_index = 0
- else:
- line_index = entry.lineno - entry.getfirstlinesource()
- short = style == "short"
- reprargs = self.repr_args(entry) if not short else None
- s = self.get_source(source, line_index, excinfo, short=short)
- lines.extend(s)
- if short:
- message = "in %s" % (entry.name)
- else:
- message = excinfo and excinfo.typename or ""
- entry_path = entry.path
- path = self._makepath(entry_path)
- reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
- localsrepr = self.repr_locals(entry.locals)
- return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
- elif style == "value":
- if excinfo:
- lines.extend(str(excinfo.value).split("\n"))
- return ReprEntry(lines, None, None, None, style)
- else:
- if excinfo:
- lines.extend(self.get_exconly(excinfo, indent=4))
- return ReprEntry(lines, None, None, None, style)
- def _makepath(self, path: Union[Path, str]) -> str:
- if not self.abspath and isinstance(path, Path):
- try:
- np = bestrelpath(Path.cwd(), path)
- except OSError:
- return str(path)
- if len(np) < len(str(path)):
- return np
- return str(path)
- def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
- traceback = excinfo.traceback
- if callable(self.tbfilter):
- traceback = self.tbfilter(excinfo)
- elif self.tbfilter:
- traceback = traceback.filter(excinfo)
- if isinstance(excinfo.value, RecursionError):
- traceback, extraline = self._truncate_recursive_traceback(traceback)
- else:
- extraline = None
- if not traceback:
- if extraline is None:
- extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
- entries = [self.repr_traceback_entry(None, excinfo)]
- return ReprTraceback(entries, extraline, style=self.style)
- last = traceback[-1]
- if self.style == "value":
- entries = [self.repr_traceback_entry(last, excinfo)]
- return ReprTraceback(entries, None, style=self.style)
- entries = [
- self.repr_traceback_entry(entry, excinfo if last == entry else None)
- for entry in traceback
- ]
- return ReprTraceback(entries, extraline, style=self.style)
- def _truncate_recursive_traceback(
- self, traceback: Traceback
- ) -> Tuple[Traceback, Optional[str]]:
- """Truncate the given recursive traceback trying to find the starting
- point of the recursion.
- The detection is done by going through each traceback entry and
- finding the point in which the locals of the frame are equal to the
- locals of a previous frame (see ``recursionindex()``).
- Handle the situation where the recursion process might raise an
- exception (for example comparing numpy arrays using equality raises a
- TypeError), in which case we do our best to warn the user of the
- error and show a limited traceback.
- """
- try:
- recursionindex = traceback.recursionindex()
- except Exception as e:
- max_frames = 10
- extraline: Optional[str] = (
- "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
- " The following exception happened when comparing locals in the stack frame:\n"
- " {exc_type}: {exc_msg}\n"
- " Displaying first and last {max_frames} stack frames out of {total}."
- ).format(
- exc_type=type(e).__name__,
- exc_msg=str(e),
- max_frames=max_frames,
- total=len(traceback),
- )
- # Type ignored because adding two instances of a List subtype
- # currently incorrectly has type List instead of the subtype.
- traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
- else:
- if recursionindex is not None:
- extraline = "!!! Recursion detected (same locals & position)"
- traceback = traceback[: recursionindex + 1]
- else:
- extraline = None
- return traceback, extraline
- def repr_excinfo(
- self, excinfo: ExceptionInfo[BaseException]
- ) -> "ExceptionChainRepr":
- repr_chain: List[
- Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
- ] = []
- e: Optional[BaseException] = excinfo.value
- excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
- descr = None
- seen: Set[int] = set()
- while e is not None and id(e) not in seen:
- seen.add(id(e))
- if excinfo_:
- # Fall back to native traceback as a temporary workaround until
- # full support for exception groups added to ExceptionInfo.
- # See https://github.com/pytest-dev/pytest/issues/9159
- if isinstance(e, BaseExceptionGroup):
- reprtraceback: Union[
- ReprTracebackNative, ReprTraceback
- ] = ReprTracebackNative(
- traceback.format_exception(
- type(excinfo_.value),
- excinfo_.value,
- excinfo_.traceback[0]._rawentry,
- )
- )
- else:
- reprtraceback = self.repr_traceback(excinfo_)
- reprcrash = excinfo_._getreprcrash()
- else:
- # Fallback to native repr if the exception doesn't have a traceback:
- # ExceptionInfo objects require a full traceback to work.
- reprtraceback = ReprTracebackNative(
- traceback.format_exception(type(e), e, None)
- )
- reprcrash = None
- repr_chain += [(reprtraceback, reprcrash, descr)]
- if e.__cause__ is not None and self.chain:
- e = e.__cause__
- excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
- descr = "The above exception was the direct cause of the following exception:"
- elif (
- e.__context__ is not None and not e.__suppress_context__ and self.chain
- ):
- e = e.__context__
- excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
- descr = "During handling of the above exception, another exception occurred:"
- else:
- e = None
- repr_chain.reverse()
- return ExceptionChainRepr(repr_chain)
- @dataclasses.dataclass(eq=False)
- class TerminalRepr:
- def __str__(self) -> str:
- # FYI this is called from pytest-xdist's serialization of exception
- # information.
- io = StringIO()
- tw = TerminalWriter(file=io)
- self.toterminal(tw)
- return io.getvalue().strip()
- def __repr__(self) -> str:
- return f"<{self.__class__} instance at {id(self):0x}>"
- def toterminal(self, tw: TerminalWriter) -> None:
- raise NotImplementedError()
- # This class is abstract -- only subclasses are instantiated.
- @dataclasses.dataclass(eq=False)
- class ExceptionRepr(TerminalRepr):
- # Provided by subclasses.
- reprtraceback: "ReprTraceback"
- reprcrash: Optional["ReprFileLocation"]
- sections: List[Tuple[str, str, str]] = dataclasses.field(
- init=False, default_factory=list
- )
- def addsection(self, name: str, content: str, sep: str = "-") -> None:
- self.sections.append((name, content, sep))
- def toterminal(self, tw: TerminalWriter) -> None:
- for name, content, sep in self.sections:
- tw.sep(sep, name)
- tw.line(content)
- @dataclasses.dataclass(eq=False)
- class ExceptionChainRepr(ExceptionRepr):
- chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
- def __init__(
- self,
- chain: Sequence[
- Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
- ],
- ) -> None:
- # reprcrash and reprtraceback of the outermost (the newest) exception
- # in the chain.
- super().__init__(
- reprtraceback=chain[-1][0],
- reprcrash=chain[-1][1],
- )
- self.chain = chain
- def toterminal(self, tw: TerminalWriter) -> None:
- for element in self.chain:
- element[0].toterminal(tw)
- if element[2] is not None:
- tw.line("")
- tw.line(element[2], yellow=True)
- super().toterminal(tw)
- @dataclasses.dataclass(eq=False)
- class ReprExceptionInfo(ExceptionRepr):
- reprtraceback: "ReprTraceback"
- reprcrash: Optional["ReprFileLocation"]
- def toterminal(self, tw: TerminalWriter) -> None:
- self.reprtraceback.toterminal(tw)
- super().toterminal(tw)
- @dataclasses.dataclass(eq=False)
- class ReprTraceback(TerminalRepr):
- reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
- extraline: Optional[str]
- style: "_TracebackStyle"
- entrysep: ClassVar = "_ "
- def toterminal(self, tw: TerminalWriter) -> None:
- # The entries might have different styles.
- for i, entry in enumerate(self.reprentries):
- if entry.style == "long":
- tw.line("")
- entry.toterminal(tw)
- if i < len(self.reprentries) - 1:
- next_entry = self.reprentries[i + 1]
- if (
- entry.style == "long"
- or entry.style == "short"
- and next_entry.style == "long"
- ):
- tw.sep(self.entrysep)
- if self.extraline:
- tw.line(self.extraline)
- class ReprTracebackNative(ReprTraceback):
- def __init__(self, tblines: Sequence[str]) -> None:
- self.reprentries = [ReprEntryNative(tblines)]
- self.extraline = None
- self.style = "native"
- @dataclasses.dataclass(eq=False)
- class ReprEntryNative(TerminalRepr):
- lines: Sequence[str]
- style: ClassVar["_TracebackStyle"] = "native"
- def toterminal(self, tw: TerminalWriter) -> None:
- tw.write("".join(self.lines))
- @dataclasses.dataclass(eq=False)
- class ReprEntry(TerminalRepr):
- lines: Sequence[str]
- reprfuncargs: Optional["ReprFuncArgs"]
- reprlocals: Optional["ReprLocals"]
- reprfileloc: Optional["ReprFileLocation"]
- style: "_TracebackStyle"
- def _write_entry_lines(self, tw: TerminalWriter) -> None:
- """Write the source code portions of a list of traceback entries with syntax highlighting.
- Usually entries are lines like these:
- " x = 1"
- "> assert x == 2"
- "E assert 1 == 2"
- This function takes care of rendering the "source" portions of it (the lines without
- the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
- character, as doing so might break line continuations.
- """
- if not self.lines:
- return
- # separate indents and source lines that are not failures: we want to
- # highlight the code but not the indentation, which may contain markers
- # such as "> assert 0"
- fail_marker = f"{FormattedExcinfo.fail_marker} "
- indent_size = len(fail_marker)
- indents: List[str] = []
- source_lines: List[str] = []
- failure_lines: List[str] = []
- for index, line in enumerate(self.lines):
- is_failure_line = line.startswith(fail_marker)
- if is_failure_line:
- # from this point on all lines are considered part of the failure
- failure_lines.extend(self.lines[index:])
- break
- else:
- if self.style == "value":
- source_lines.append(line)
- else:
- indents.append(line[:indent_size])
- source_lines.append(line[indent_size:])
- tw._write_source(source_lines, indents)
- # failure lines are always completely red and bold
- for line in failure_lines:
- tw.line(line, bold=True, red=True)
- def toterminal(self, tw: TerminalWriter) -> None:
- if self.style == "short":
- if self.reprfileloc:
- self.reprfileloc.toterminal(tw)
- self._write_entry_lines(tw)
- if self.reprlocals:
- self.reprlocals.toterminal(tw, indent=" " * 8)
- return
- if self.reprfuncargs:
- self.reprfuncargs.toterminal(tw)
- self._write_entry_lines(tw)
- if self.reprlocals:
- tw.line("")
- self.reprlocals.toterminal(tw)
- if self.reprfileloc:
- if self.lines:
- tw.line("")
- self.reprfileloc.toterminal(tw)
- def __str__(self) -> str:
- return "{}\n{}\n{}".format(
- "\n".join(self.lines), self.reprlocals, self.reprfileloc
- )
- @dataclasses.dataclass(eq=False)
- class ReprFileLocation(TerminalRepr):
- path: str
- lineno: int
- message: str
- def __post_init__(self) -> None:
- self.path = str(self.path)
- def toterminal(self, tw: TerminalWriter) -> None:
- # Filename and lineno output for each entry, using an output format
- # that most editors understand.
- msg = self.message
- i = msg.find("\n")
- if i != -1:
- msg = msg[:i]
- tw.write(self.path, bold=True, red=True)
- tw.line(f":{self.lineno}: {msg}")
- @dataclasses.dataclass(eq=False)
- class ReprLocals(TerminalRepr):
- lines: Sequence[str]
- def toterminal(self, tw: TerminalWriter, indent="") -> None:
- for line in self.lines:
- tw.line(indent + line)
- @dataclasses.dataclass(eq=False)
- class ReprFuncArgs(TerminalRepr):
- args: Sequence[Tuple[str, object]]
- def toterminal(self, tw: TerminalWriter) -> None:
- if self.args:
- linesofar = ""
- for name, value in self.args:
- ns = f"{name} = {value}"
- if len(ns) + len(linesofar) + 2 > tw.fullwidth:
- if linesofar:
- tw.line(linesofar)
- linesofar = ns
- else:
- if linesofar:
- linesofar += ", " + ns
- else:
- linesofar = ns
- if linesofar:
- tw.line(linesofar)
- tw.line("")
- def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
- """Return source location (path, lineno) for the given object.
- If the source cannot be determined return ("", -1).
- The line number is 0-based.
- """
- # xxx let decorators etc specify a sane ordering
- # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
- # in 6ec13a2b9. It ("place_as") appears to be something very custom.
- obj = get_real_func(obj)
- if hasattr(obj, "place_as"):
- obj = obj.place_as # type: ignore[attr-defined]
- try:
- code = Code.from_function(obj)
- except TypeError:
- try:
- fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
- except TypeError:
- return "", -1
- fspath = fn and absolutepath(fn) or ""
- lineno = -1
- if fspath:
- try:
- _, lineno = findsource(obj)
- except OSError:
- pass
- return fspath, lineno
- return code.path, code.firstlineno
- # Relative paths that we use to filter traceback entries from appearing to the user;
- # see filter_traceback.
- # note: if we need to add more paths than what we have now we should probably use a list
- # for better maintenance.
- _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
- # pluggy is either a package or a single module depending on the version
- if _PLUGGY_DIR.name == "__init__.py":
- _PLUGGY_DIR = _PLUGGY_DIR.parent
- _PYTEST_DIR = Path(_pytest.__file__).parent
- def filter_traceback(entry: TracebackEntry) -> bool:
- """Return True if a TracebackEntry instance should be included in tracebacks.
- We hide traceback entries of:
- * dynamically generated code (no code to show up for it);
- * internal traceback from pytest or its internal libraries, py and pluggy.
- """
- # entry.path might sometimes return a str object when the entry
- # points to dynamically generated code.
- # See https://bitbucket.org/pytest-dev/py/issues/71.
- raw_filename = entry.frame.code.raw.co_filename
- is_generated = "<" in raw_filename and ">" in raw_filename
- if is_generated:
- return False
- # entry.path might point to a non-existing file, in which case it will
- # also return a str object. See #1133.
- p = Path(entry.path)
- parents = p.parents
- if _PLUGGY_DIR in parents:
- return False
- if _PYTEST_DIR in parents:
- return False
- return True
|