| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567 |
- # mypy: allow-untyped-defs
- from __future__ import annotations
- import ast
- from collections.abc import Callable
- from collections.abc import Iterable
- from collections.abc import Mapping
- from collections.abc import Sequence
- import dataclasses
- import inspect
- from inspect import CO_VARARGS
- from inspect import CO_VARKEYWORDS
- from io import StringIO
- import os
- from pathlib import Path
- import re
- import sys
- from traceback import extract_tb
- from traceback import format_exception
- from traceback import format_exception_only
- from traceback import FrameSummary
- from types import CodeType
- from types import FrameType
- from types import TracebackType
- from typing import Any
- from typing import ClassVar
- from typing import Final
- from typing import final
- from typing import Generic
- from typing import Literal
- from typing import overload
- from typing import SupportsIndex
- 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 get_real_func
- from _pytest.deprecated import check_ispytest
- from _pytest.pathlib import absolutepath
- from _pytest.pathlib import bestrelpath
- if sys.version_info < (3, 11):
- from exceptiongroup import BaseExceptionGroup
- TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
- EXCEPTION_OR_MORE = Union[type[BaseException], tuple[type[BaseException], ...]]
- 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) -> 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) -> Source | None:
- """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: Literal["short", "long"] | None = None,
- ) -> None:
- self._rawentry: Final = rawentry
- self._repr_style: Final = repr_style
- def with_repr_style(
- self, repr_style: Literal["short", "long"] | None
- ) -> TracebackEntry:
- return TracebackEntry(self._rawentry, repr_style)
- @property
- def lineno(self) -> int:
- return self._rawentry.tb_lineno - 1
- def get_python_framesummary(self) -> FrameSummary:
- # Python's built-in traceback module implements all the nitty gritty
- # details to get column numbers of out frames.
- stack_summary = extract_tb(self._rawentry, limit=1)
- return stack_summary[0]
- # Column and end line numbers introduced in python 3.11
- if sys.version_info < (3, 11):
- @property
- def end_lineno_relative(self) -> int | None:
- return None
- @property
- def colno(self) -> int | None:
- return None
- @property
- def end_colno(self) -> int | None:
- return None
- else:
- @property
- def end_lineno_relative(self) -> int | None:
- frame_summary = self.get_python_framesummary()
- if frame_summary.end_lineno is None: # pragma: no cover
- return None
- return frame_summary.end_lineno - 1 - self.frame.code.firstlineno
- @property
- def colno(self) -> int | None:
- """Starting byte offset of the expression in the traceback entry."""
- return self.get_python_framesummary().colno
- @property
- def end_colno(self) -> int | None:
- """Ending byte offset of the expression in the traceback entry."""
- return self.get_python_framesummary().end_colno
- @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 f"<TracebackEntry {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) -> 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: dict[str | Path, ast.AST] | None = None
- ) -> Source | None:
- """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: ExceptionInfo[BaseException] | None) -> 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: bool | Callable[[ExceptionInfo[BaseException] | None], 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 f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n"
- @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: TracebackType | Iterable[TracebackEntry],
- ) -> None:
- """Initialize from given python traceback object and ExceptionInfo."""
- if isinstance(tb, TracebackType):
- def f(cur: TracebackType) -> Iterable[TracebackEntry]:
- cur_: TracebackType | None = 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: os.PathLike[str] | str | None = None,
- lineno: int | None = None,
- firstlineno: int | None = None,
- excludepath: os.PathLike[str] | None = 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: SupportsIndex | slice) -> TracebackEntry | Traceback:
- if isinstance(key, slice):
- return self.__class__(super().__getitem__(key))
- else:
- return super().__getitem__(key)
- def filter(
- self,
- excinfo_or_fn: 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) -> int | None:
- """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
- values = cache.setdefault(key, [])
- # Since Python 3.13 f_locals is a proxy, freeze it.
- loc = dict(entry.frame.f_locals)
- if values:
- for otherloc in values:
- if otherloc == loc:
- return i
- values.append(loc)
- return None
- def stringify_exception(
- exc: BaseException, include_subexception_msg: bool = True
- ) -> str:
- try:
- notes = getattr(exc, "__notes__", [])
- except KeyError:
- # Workaround for https://github.com/python/cpython/issues/98778 on
- # Python <= 3.9, and some 3.10 and 3.11 patch versions.
- HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
- if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
- notes = []
- else: # pragma: no cover
- # exception not related to above bug, reraise
- raise
- if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
- message = exc.message
- else:
- message = str(exc)
- return "\n".join(
- [
- message,
- *notes,
- ]
- )
- 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: tuple[type[E], E, TracebackType] | None
- _striptext: str
- _traceback: Traceback | None
- def __init__(
- self,
- excinfo: tuple[type[E], E, TracebackType] | None,
- striptext: str = "",
- traceback: Traceback | None = 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: str | None = 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: str | None = 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: str | None = 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 f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={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).
- """
- def _get_single_subexc(
- eg: BaseExceptionGroup[BaseException],
- ) -> BaseException | None:
- if len(eg.exceptions) != 1:
- return None
- if isinstance(e := eg.exceptions[0], BaseExceptionGroup):
- return _get_single_subexc(e)
- return e
- if (
- tryshort
- and isinstance(self.value, BaseExceptionGroup)
- and (subexc := _get_single_subexc(self.value)) is not None
- ):
- return f"{subexc!r} [single exception in {type(self.value).__name__}]"
- 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: EXCEPTION_OR_MORE) -> 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) -> ReprFileLocation | None:
- # 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: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
- funcargs: bool = False,
- truncate_locals: bool = True,
- truncate_args: bool = True,
- chain: bool = True,
- ) -> 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 truncate_args:
- With ``showargs==True``, make sure args 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(
- 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,
- truncate_args=truncate_args,
- chain=chain,
- )
- return fmt.repr_excinfo(self)
- def match(self, regexp: str | re.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 = stringify_exception(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
- def _group_contains(
- self,
- exc_group: BaseExceptionGroup[BaseException],
- expected_exception: EXCEPTION_OR_MORE,
- match: str | re.Pattern[str] | None,
- target_depth: int | None = None,
- current_depth: int = 1,
- ) -> bool:
- """Return `True` if a `BaseExceptionGroup` contains a matching exception."""
- if (target_depth is not None) and (current_depth > target_depth):
- # already descended past the target depth
- return False
- for exc in exc_group.exceptions:
- if isinstance(exc, BaseExceptionGroup):
- if self._group_contains(
- exc, expected_exception, match, target_depth, current_depth + 1
- ):
- return True
- if (target_depth is not None) and (current_depth != target_depth):
- # not at the target depth, no match
- continue
- if not isinstance(exc, expected_exception):
- continue
- if match is not None:
- value = stringify_exception(exc)
- if not re.search(match, value):
- continue
- return True
- return False
- def group_contains(
- self,
- expected_exception: EXCEPTION_OR_MORE,
- *,
- match: str | re.Pattern[str] | None = None,
- depth: int | None = None,
- ) -> bool:
- """Check whether a captured exception group contains a matching exception.
- :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
- The expected exception type, or a tuple if one of multiple possible
- exception types are expected.
- :param str | re.Pattern[str] | None match:
- If specified, a string containing a regular expression,
- or a regular expression object, that is tested against the string
- representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
- using :func:`re.search`.
- To match a literal string that may contain :ref:`special characters
- <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
- :param Optional[int] depth:
- If `None`, will search for a matching exception at any nesting depth.
- If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
- the exceptions contained within the topmost exception group).
- .. versionadded:: 8.0
- .. warning::
- This helper makes it easy to check for the presence of specific exceptions,
- but it is very bad for checking that the group does *not* contain
- *any other exceptions*.
- You should instead consider using :class:`pytest.RaisesGroup`
- """
- msg = "Captured exception is not an instance of `BaseExceptionGroup`"
- assert isinstance(self.value, BaseExceptionGroup), msg
- msg = "`depth` must be >= 1 if specified"
- assert (depth is None) or (depth >= 1), msg
- return self._group_contains(self.value, expected_exception, match, depth)
- if TYPE_CHECKING:
- from typing_extensions import TypeAlias
- # Type alias for the `tbfilter` setting:
- # bool: If True, it should be filtered using Traceback.filter()
- # callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
- TracebackFilter: TypeAlias = Union[
- bool, Callable[[ExceptionInfo[BaseException]], Traceback]
- ]
- @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: TracebackFilter = True
- funcargs: bool = False
- truncate_locals: bool = True
- truncate_args: bool = True
- chain: bool = True
- astcache: dict[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) -> Source | None:
- source = entry.getsource(self.astcache)
- if source is not None:
- source = source.deindent()
- return source
- def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
- if self.funcargs:
- args = []
- for argname, argvalue in entry.frame.getargs(var=True):
- if self.truncate_args:
- str_repr = saferepr(argvalue)
- else:
- str_repr = saferepr(argvalue, maxsize=None)
- args.append((argname, str_repr))
- return ReprFuncArgs(args)
- return None
- def get_source(
- self,
- source: Source | None,
- line_index: int = -1,
- excinfo: ExceptionInfo[BaseException] | None = None,
- short: bool = False,
- end_line_index: int | None = None,
- colno: int | None = None,
- end_colno: int | None = None,
- ) -> 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())
- lines.extend(
- self.get_highlight_arrows_for_line(
- raw_line=source.raw_lines[line_index],
- line=source.lines[line_index].strip(),
- lineno=line_index,
- end_lineno=end_line_index,
- colno=colno,
- end_colno=end_colno,
- )
- )
- else:
- for line in source.lines[:line_index]:
- lines.append(space_prefix + line)
- lines.append(self.flow_marker + " " + source.lines[line_index])
- lines.extend(
- self.get_highlight_arrows_for_line(
- raw_line=source.raw_lines[line_index],
- line=source.lines[line_index],
- lineno=line_index,
- end_lineno=end_line_index,
- colno=colno,
- end_colno=end_colno,
- )
- )
- 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_highlight_arrows_for_line(
- self,
- line: str,
- raw_line: str,
- lineno: int | None,
- end_lineno: int | None,
- colno: int | None,
- end_colno: int | None,
- ) -> list[str]:
- """Return characters highlighting a source line.
- Example with colno and end_colno pointing to the bar expression:
- "foo() + bar()"
- returns " ^^^^^"
- """
- if lineno != end_lineno:
- # Don't handle expressions that span multiple lines.
- return []
- if colno is None or end_colno is None:
- # Can't do anything without column information.
- return []
- num_stripped_chars = len(raw_line) - len(line)
- start_char_offset = _byte_offset_to_character_offset(raw_line, colno)
- end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno)
- num_carets = end_char_offset - start_char_offset
- # If the highlight would span the whole line, it is redundant, don't
- # show it.
- if num_carets >= len(line.strip()):
- return []
- highlights = " "
- highlights += " " * (start_char_offset - num_stripped_chars + 1)
- highlights += "^" * num_carets
- return [highlights]
- 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]) -> ReprLocals | None:
- 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: TracebackEntry | None,
- excinfo: ExceptionInfo[BaseException] | None = 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
- end_line_index, colno, end_colno = None, None, None
- else:
- line_index = entry.relline
- end_line_index = entry.end_lineno_relative
- colno = entry.colno
- end_colno = entry.end_colno
- short = style == "short"
- reprargs = self.repr_args(entry) if not short else None
- s = self.get_source(
- source=source,
- line_index=line_index,
- excinfo=excinfo,
- short=short,
- end_line_index=end_line_index,
- colno=colno,
- end_colno=end_colno,
- )
- lines.extend(s)
- if short:
- message = f"in {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: 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 = filter_excinfo_traceback(self.tbfilter, 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, str | None]:
- """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: str | None = (
- "!!! 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"
- f" {type(e).__name__}: {e!s}\n"
- f" Displaying first and last {max_frames} stack frames out of {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, ReprFileLocation | None, str | None]] = []
- e: BaseException | None = excinfo.value
- excinfo_: ExceptionInfo[BaseException] | None = 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
- reprtraceback: ReprTraceback | ReprTracebackNative
- if isinstance(e, BaseExceptionGroup):
- # don't filter any sub-exceptions since they shouldn't have any internal frames
- traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
- reprtraceback = ReprTracebackNative(
- format_exception(
- type(excinfo.value),
- excinfo.value,
- 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(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: ReprFileLocation | None
- 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, ReprFileLocation | None, str | None]]
- def __init__(
- self,
- chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
- ) -> 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: ReprFileLocation | None
- def toterminal(self, tw: TerminalWriter) -> None:
- self.reprtraceback.toterminal(tw)
- super().toterminal(tw)
- @dataclasses.dataclass(eq=False)
- class ReprTraceback(TerminalRepr):
- reprentries: Sequence[ReprEntry | ReprEntryNative]
- extraline: str | None
- 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: ReprFuncArgs | None
- reprlocals: ReprLocals | None
- reprfileloc: ReprFileLocation | None
- 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
- if self.style == "value":
- # Using tw.write instead of tw.line for testing purposes due to TWMock implementation;
- # lines written with TWMock.line and TWMock._write_source cannot be distinguished
- # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE
- for line in self.lines:
- tw.write(line)
- tw.write("\n")
- 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:
- 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[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
- 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
- def _byte_offset_to_character_offset(str, offset):
- """Converts a byte based offset in a string to a code-point."""
- as_utf8 = str.encode("utf-8")
- return len(as_utf8[:offset].decode("utf-8", errors="replace"))
- # 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
- def filter_excinfo_traceback(
- tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
- ) -> Traceback:
- """Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
- if callable(tbfilter):
- return tbfilter(excinfo)
- elif tbfilter:
- return excinfo.traceback.filter(excinfo)
- else:
- return excinfo.traceback
|