You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1260 lines
42KB

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