1 import ast

2 import inspect

3 import os

4 import re

5 import sys

6 import traceback

7 from inspect import CO_VARARGS

8 from inspect import CO_VARKEYWORDS

9 from io import StringIO

10 from pathlib import Path

11 from traceback import format_exception_only

12 from types import CodeType

13 from types import FrameType

14 from types import TracebackType

15 from typing import Any

16 from typing import Callable

17 from typing import ClassVar

18 from typing import Dict

19 from typing import Generic

20 from typing import Iterable

21 from typing import List

22 from typing import Mapping

23 from typing import Optional

24 from typing import overload

25 from typing import Pattern

26 from typing import Sequence

27 from typing import Set

28 from typing import Tuple

29 from typing import Type

30 from typing import TYPE_CHECKING

31 from typing import TypeVar

32 from typing import Union

33 from weakref import ref

34

35 import attr

36 import pluggy

37

38 import _pytest

39 from _pytest._code.source import findsource

40 from _pytest._code.source import getrawcode

41 from _pytest._code.source import getstatementrange_ast

42 from _pytest._code.source import Source

43 from _pytest._io import TerminalWriter

44 from _pytest._io.saferepr import safeformat

45 from _pytest._io.saferepr import saferepr

46 from _pytest.compat import final

47 from _pytest.compat import get_real_func

48 from _pytest.deprecated import check_ispytest

49 from _pytest.pathlib import absolutepath

50 from _pytest.pathlib import bestrelpath

51

52 if TYPE_CHECKING:

53 from typing_extensions import Literal

54 from typing_extensions import SupportsIndex

55 from weakref import ReferenceType

56

57 _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]

58

59 if sys.version_info[:2] < (3, 11):

60 from exceptiongroup import BaseExceptionGroup

61

62

63 class Code:

64 """Wrapper around Python code objects."""

65

66 __slots__ = ("raw",)

67

68 def __init__(self, obj: CodeType) -> None:

69 self.raw = obj

70

71 @classmethod

72 def from_function(cls, obj: object) -> "Code":

73 return cls(getrawcode(obj))

74

75 def __eq__(self, other):

76 return self.raw == other.raw

77

78 # Ignore type because of https://github.com/python/mypy/issues/4266.

79 __hash__ = None # type: ignore

80

81 @property

82 def firstlineno(self) -> int:

83 return self.raw.co_firstlineno - 1

84

85 @property

86 def name(self) -> str:

87 return self.raw.co_name

88

89 @property

90 def path(self) -> Union[Path, str]:

91 """Return a path object pointing to source code, or an ``str`` in

92 case of ``OSError`` / non-existing file."""

93 if not self.raw.co_filename:

94 return ""

95 try:

96 p = absolutepath(self.raw.co_filename)

97 # maybe don't try this checking

98 if not p.exists():

99 raise OSError("path check failed.")

100 return p

101 except OSError:

102 # XXX maybe try harder like the weird logic

103 # in the standard lib [linecache.updatecache] does?

104 return self.raw.co_filename

105

106 @property

107 def fullsource(self) -> Optional["Source"]:

108 """Return a _pytest._code.Source object for the full source file of the code."""

109 full, _ = findsource(self.raw)

110 return full

111

112 def source(self) -> "Source":

113 """Return a _pytest._code.Source object for the code object's source only."""

114 # return source only for that part of code

115 return Source(self.raw)

116

117 def getargs(self, var: bool = False) -> Tuple[str, ...]:

118 """Return a tuple with the argument names for the code object.

119

120 If 'var' is set True also return the names of the variable and

121 keyword arguments when present.

122 """

123 # Handy shortcut for getting args.

124 raw = self.raw

125 argcount = raw.co_argcount

126 if var:

127 argcount += raw.co_flags & CO_VARARGS

128 argcount += raw.co_flags & CO_VARKEYWORDS

129 return raw.co_varnames[:argcount]

130

131

132 class Frame:

133 """Wrapper around a Python frame holding f_locals and f_globals

134 in which expressions can be evaluated."""

135

136 __slots__ = ("raw",)

137

138 def __init__(self, frame: FrameType) -> None:

139 self.raw = frame

140

141 @property

142 def lineno(self) -> int:

143 return self.raw.f_lineno - 1

144

145 @property

146 def f_globals(self) -> Dict[str, Any]:

147 return self.raw.f_globals

148

149 @property

150 def f_locals(self) -> Dict[str, Any]:

151 return self.raw.f_locals

152

153 @property

154 def code(self) -> Code:

155 return Code(self.raw.f_code)

156

157 @property

158 def statement(self) -> "Source":

159 """Statement this frame is at."""

160 if self.code.fullsource is None:

161 return Source("")

162 return self.code.fullsource.getstatement(self.lineno)

163

164 def eval(self, code, **vars):

165 """Evaluate 'code' in the frame.

166

167 'vars' are optional additional local variables.

168

169 Returns the result of the evaluation.

170 """

171 f_locals = self.f_locals.copy()

172 f_locals.update(vars)

173 return eval(code, self.f_globals, f_locals)

174

175 def repr(self, object: object) -> str:

176 """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""

177 return saferepr(object)

178

179 def getargs(self, var: bool = False):

180 """Return a list of tuples (name, value) for all arguments.

181

182 If 'var' is set True, also include the variable and keyword arguments

183 when present.

184 """

185 retval = []

186 for arg in self.code.getargs(var):

187 try:

188 retval.append((arg, self.f_locals[arg]))

189 except KeyError:

190 pass # this can occur when using Psyco

191 return retval

192

193

194 class TracebackEntry:

195 """A single entry in a Traceback."""

196

197 __slots__ = ("_rawentry", "_excinfo", "_repr_style")

198

199 def __init__(

200 self,

201 rawentry: TracebackType,

202 excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,

203 ) -> None:

204 self._rawentry = rawentry

205 self._excinfo = excinfo

206 self._repr_style: Optional['Literal["short", "long"]'] = None

207

208 @property

209 def lineno(self) -> int:

210 return self._rawentry.tb_lineno - 1

211

212 def set_repr_style(self, mode: "Literal['short', 'long']") -> None:

213 assert mode in ("short", "long")

214 self._repr_style = mode

215

216 @property

217 def frame(self) -> Frame:

218 return Frame(self._rawentry.tb_frame)

219

220 @property

221 def relline(self) -> int:

222 return self.lineno - self.frame.code.firstlineno

223

224 def __repr__(self) -> str:

225 return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)

226

227 @property

228 def statement(self) -> "Source":

229 """_pytest._code.Source object for the current statement."""

230 source = self.frame.code.fullsource

231 assert source is not None

232 return source.getstatement(self.lineno)

233

234 @property

235 def path(self) -> Union[Path, str]:

236 """Path to the source code."""

237 return self.frame.code.path

238

239 @property

240 def locals(self) -> Dict[str, Any]:

241 """Locals of underlying frame."""

242 return self.frame.f_locals

243

244 def getfirstlinesource(self) -> int:

245 return self.frame.code.firstlineno

246

247 def getsource(

248 self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None

249 ) -> Optional["Source"]:

250 """Return failing source code."""

251 # we use the passed in astcache to not reparse asttrees

252 # within exception info printing

253 source = self.frame.code.fullsource

254 if source is None:

255 return None

256 key = astnode = None

257 if astcache is not None:

258 key = self.frame.code.path

259 if key is not None:

260 astnode = astcache.get(key, None)

261 start = self.getfirstlinesource()

262 try:

263 astnode, _, end = getstatementrange_ast(

264 self.lineno, source, astnode=astnode

265 )

266 except SyntaxError:

267 end = self.lineno + 1

268 else:

269 if key is not None and astcache is not None:

270 astcache[key] = astnode

271 return source[start:end]

272

273 source = property(getsource)

274

275 def ishidden(self) -> bool:

276 """Return True if the current frame has a var __tracebackhide__

277 resolving to True.

278

279 If __tracebackhide__ is a callable, it gets called with the

280 ExceptionInfo instance and can decide whether to hide the traceback.

281

282 Mostly for internal use.

283 """

284 tbh: Union[

285 bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]

286 ] = False

287 for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):

288 # in normal cases, f_locals and f_globals are dictionaries

289 # however via `exec(...)` / `eval(...)` they can be other types

290 # (even incorrect types!).

291 # as such, we suppress all exceptions while accessing __tracebackhide__

292 try:

293 tbh = maybe_ns_dct["__tracebackhide__"]

294 except Exception:

295 pass

296 else:

297 break

298 if tbh and callable(tbh):

299 return tbh(None if self._excinfo is None else self._excinfo())

300 return tbh

301

302 def __str__(self) -> str:

303 name = self.frame.code.name

304 try:

305 line = str(self.statement).lstrip()

306 except KeyboardInterrupt:

307 raise

308 except BaseException:

309 line = "???"

310 # This output does not quite match Python's repr for traceback entries,

311 # but changing it to do so would break certain plugins. See

312 # https://github.com/pytest-dev/pytest/pull/7535/ for details.

313 return " File %r:%d in %s\n %s\n" % (

314 str(self.path),

315 self.lineno + 1,

316 name,

317 line,

318 )

319

320 @property

321 def name(self) -> str:

322 """co_name of underlying code."""

323 return self.frame.code.raw.co_name

324

325

326 class Traceback(List[TracebackEntry]):

327 """Traceback objects encapsulate and offer higher level access to Traceback entries."""

328

329 def __init__(

330 self,

331 tb: Union[TracebackType, Iterable[TracebackEntry]],

332 excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,

333 ) -> None:

334 """Initialize from given python traceback object and ExceptionInfo."""

335 self._excinfo = excinfo

336 if isinstance(tb, TracebackType):

337

338 def f(cur: TracebackType) -> Iterable[TracebackEntry]:

339 cur_: Optional[TracebackType] = cur

340 while cur_ is not None:

341 yield TracebackEntry(cur_, excinfo=excinfo)

342 cur_ = cur_.tb_next

343

344 super().__init__(f(tb))

345 else:

346 super().__init__(tb)

347

348 def cut(

349 self,

350 path: Optional[Union["os.PathLike[str]", str]] = None,

351 lineno: Optional[int] = None,

352 firstlineno: Optional[int] = None,

353 excludepath: Optional["os.PathLike[str]"] = None,

354 ) -> "Traceback":

355 """Return a Traceback instance wrapping part of this Traceback.

356

357 By providing any combination of path, lineno and firstlineno, the

358 first frame to start the to-be-returned traceback is determined.

359

360 This allows cutting the first part of a Traceback instance e.g.

361 for formatting reasons (removing some uninteresting bits that deal

362 with handling of the exception/traceback).

363 """

364 path_ = None if path is None else os.fspath(path)

365 excludepath_ = None if excludepath is None else os.fspath(excludepath)

366 for x in self:

367 code = x.frame.code

368 codepath = code.path

369 if path is not None and str(codepath) != path_:

370 continue

371 if (

372 excludepath is not None

373 and isinstance(codepath, Path)

374 and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]

375 ):

376 continue

377 if lineno is not None and x.lineno != lineno:

378 continue

379 if firstlineno is not None and x.frame.code.firstlineno != firstlineno:

380 continue

381 return Traceback(x._rawentry, self._excinfo)

382 return self

383

384 @overload

385 def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:

386 ...

387

388 @overload

389 def __getitem__(self, key: slice) -> "Traceback":

390 ...

391

392 def __getitem__(

393 self, key: Union["SupportsIndex", slice]

394 ) -> Union[TracebackEntry, "Traceback"]:

395 if isinstance(key, slice):

396 return self.__class__(super().__getitem__(key))

397 else:

398 return super().__getitem__(key)

399

400 def filter(

401 self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()

402 ) -> "Traceback":

403 """Return a Traceback instance with certain items removed

404

405 fn is a function that gets a single argument, a TracebackEntry

406 instance, and should return True when the item should be added

407 to the Traceback, False when not.

408

409 By default this removes all the TracebackEntries which are hidden

410 (see ishidden() above).

411 """

412 return Traceback(filter(fn, self), self._excinfo)

413

414 def getcrashentry(self) -> TracebackEntry:

415 """Return last non-hidden traceback entry that lead to the exception of a traceback."""

416 for i in range(-1, -len(self) - 1, -1):

417 entry = self[i]

418 if not entry.ishidden():

419 return entry

420 return self[-1]

421

422 def recursionindex(self) -> Optional[int]:

423 """Return the index of the frame/TracebackEntry where recursion originates if

424 appropriate, None if no recursion occurred."""

425 cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}

426 for i, entry in enumerate(self):

427 # id for the code.raw is needed to work around

428 # the strange metaprogramming in the decorator lib from pypi

429 # which generates code objects that have hash/value equality

430 # XXX needs a test

431 key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno

432 # print "checking for recursion at", key

433 values = cache.setdefault(key, [])

434 if values:

435 f = entry.frame

436 loc = f.f_locals

437 for otherloc in values:

438 if otherloc == loc:

439 return i

440 values.append(entry.frame.f_locals)

441 return None

442

443

444 E = TypeVar("E", bound=BaseException, covariant=True)

445

446

447@final

448@attr.s(repr=False, init=False, auto_attribs=True)

449 class ExceptionInfo(Generic[E]):

450 """Wraps sys.exc_info() objects and offers help for navigating the traceback."""

451

452 _assert_start_repr: ClassVar = "AssertionError('assert "

453

454 _excinfo: Optional[Tuple[Type["E"], "E", TracebackType]]

455 _striptext: str

456 _traceback: Optional[Traceback]

457

458 def __init__(

459 self,

460 excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],

461 striptext: str = "",

462 traceback: Optional[Traceback] = None,

463 *,

464 _ispytest: bool = False,

465 ) -> None:

466 check_ispytest(_ispytest)

467 self._excinfo = excinfo

468 self._striptext = striptext

469 self._traceback = traceback

470

471 @classmethod

472 def from_exc_info(

473 cls,

474 exc_info: Tuple[Type[E], E, TracebackType],

475 exprinfo: Optional[str] = None,

476 ) -> "ExceptionInfo[E]":

477 """Return an ExceptionInfo for an existing exc_info tuple.

478

479 .. warning::

480

481 Experimental API

482

483 :param exprinfo:

484 A text string helping to determine if we should strip

485 ``AssertionError`` from the output. Defaults to the exception

486 message/``__str__()``.

487 """

488 _striptext = ""

489 if exprinfo is None and isinstance(exc_info[1], AssertionError):

490 exprinfo = getattr(exc_info[1], "msg", None)

491 if exprinfo is None:

492 exprinfo = saferepr(exc_info[1])

493 if exprinfo and exprinfo.startswith(cls._assert_start_repr):

494 _striptext = "AssertionError: "

495

496 return cls(exc_info, _striptext, _ispytest=True)

497

498 @classmethod

499 def from_current(

500 cls, exprinfo: Optional[str] = None

501 ) -> "ExceptionInfo[BaseException]":

502 """Return an ExceptionInfo matching the current traceback.

503

504 .. warning::

505

506 Experimental API

507

508 :param exprinfo:

509 A text string helping to determine if we should strip

510 ``AssertionError`` from the output. Defaults to the exception

511 message/``__str__()``.

512 """

513 tup = sys.exc_info()

514 assert tup[0] is not None, "no current exception"

515 assert tup[1] is not None, "no current exception"

516 assert tup[2] is not None, "no current exception"

517 exc_info = (tup[0], tup[1], tup[2])

518 return ExceptionInfo.from_exc_info(exc_info, exprinfo)

519

520 @classmethod

521 def for_later(cls) -> "ExceptionInfo[E]":

522 """Return an unfilled ExceptionInfo."""

523 return cls(None, _ispytest=True)

524

525 def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:

526 """Fill an unfilled ExceptionInfo created with ``for_later()``."""

527 assert self._excinfo is None, "ExceptionInfo was already filled"

528 self._excinfo = exc_info

529

530 @property

531 def type(self) -> Type[E]:

532 """The exception class."""

533 assert (

534 self._excinfo is not None

535 ), ".type can only be used after the context manager exits"

536 return self._excinfo[0]

537

538 @property

539 def value(self) -> E:

540 """The exception value."""

541 assert (

542 self._excinfo is not None

543 ), ".value can only be used after the context manager exits"

544 return self._excinfo[1]

545

546 @property

547 def tb(self) -> TracebackType:

548 """The exception raw traceback."""

549 assert (

550 self._excinfo is not None

551 ), ".tb can only be used after the context manager exits"

552 return self._excinfo[2]

553

554 @property

555 def typename(self) -> str:

556 """The type name of the exception."""

557 assert (

558 self._excinfo is not None

559 ), ".typename can only be used after the context manager exits"

560 return self.type.__name__

561

562 @property

563 def traceback(self) -> Traceback:

564 """The traceback."""

565 if self._traceback is None:

566 self._traceback = Traceback(self.tb, excinfo=ref(self))

567 return self._traceback

568

569 @traceback.setter

570 def traceback(self, value: Traceback) -> None:

571 self._traceback = value

572

573 def __repr__(self) -> str:

574 if self._excinfo is None:

575 return "<ExceptionInfo for raises contextmanager>"

576 return "<{} {} tblen={}>".format(

577 self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)

578 )

579

580 def exconly(self, tryshort: bool = False) -> str:

581 """Return the exception as a string.

582

583 When 'tryshort' resolves to True, and the exception is an

584 AssertionError, only the actual exception part of the exception

585 representation is returned (so 'AssertionError: ' is removed from

586 the beginning).

587 """

588 lines = format_exception_only(self.type, self.value)

589 text = "".join(lines)

590 text = text.rstrip()

591 if tryshort:

592 if text.startswith(self._striptext):

593 text = text[len(self._striptext) :]

594 return text

595

596 def errisinstance(

597 self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]

598 ) -> bool:

599 """Return True if the exception is an instance of exc.

600

601 Consider using ``isinstance(excinfo.value, exc)`` instead.

602 """

603 return isinstance(self.value, exc)

604

605 def _getreprcrash(self) -> "ReprFileLocation":

606 exconly = self.exconly(tryshort=True)

607 entry = self.traceback.getcrashentry()

608 path, lineno = entry.frame.code.raw.co_filename, entry.lineno

609 return ReprFileLocation(path, lineno + 1, exconly)

610

611 def getrepr(

612 self,

613 showlocals: bool = False,

614 style: "_TracebackStyle" = "long",

615 abspath: bool = False,

616 tbfilter: bool = True,

617 funcargs: bool = False,

618 truncate_locals: bool = True,

619 chain: bool = True,

620 ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:

621 """Return str()able representation of this exception info.

622

623 :param bool showlocals:

624 Show locals per traceback entry.

625 Ignored if ``style=="native"``.

626

627 :param str style:

628 long|short|no|native|value traceback style.

629

630 :param bool abspath:

631 If paths should be changed to absolute or left unchanged.

632

633 :param bool tbfilter:

634 Hide entries that contain a local variable ``__tracebackhide__==True``.

635 Ignored if ``style=="native"``.

636

637 :param bool funcargs:

638 Show fixtures ("funcargs" for legacy purposes) per traceback entry.

639

640 :param bool truncate_locals:

641 With ``showlocals==True``, make sure locals can be safely represented as strings.

642

643 :param bool chain:

644 If chained exceptions in Python 3 should be shown.

645

646 .. versionchanged:: 3.9

647

648 Added the ``chain`` parameter.

649 """

650 if style == "native":

651 return ReprExceptionInfo(

652 ReprTracebackNative(

653 traceback.format_exception(

654 self.type, self.value, self.traceback[0]._rawentry

655 )

656 ),

657 self._getreprcrash(),

658 )

659

660 fmt = FormattedExcinfo(

661 showlocals=showlocals,

662 style=style,

663 abspath=abspath,

664 tbfilter=tbfilter,

665 funcargs=funcargs,

666 truncate_locals=truncate_locals,

667 chain=chain,

668 )

669 return fmt.repr_excinfo(self)

670

671 def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":

672 """Check whether the regular expression `regexp` matches the string

673 representation of the exception using :func:`python:re.search`.

674

675 If it matches `True` is returned, otherwise an `AssertionError` is raised.

676 """

677 __tracebackhide__ = True

678 value = str(self.value)

679 msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"

680 if regexp == value:

681 msg += "\n Did you mean to `re.escape()` the regex?"

682 assert re.search(regexp, value), msg

683 # Return True to allow for "assert excinfo.match()".

684 return True

685

686

687@attr.s(auto_attribs=True)

688 class FormattedExcinfo:

689 """Presenting information about failing Functions and Generators."""

690

691 # for traceback entries

692 flow_marker: ClassVar = ">"

693 fail_marker: ClassVar = "E"

694

695 showlocals: bool = False

696 style: "_TracebackStyle" = "long"

697 abspath: bool = True

698 tbfilter: bool = True

699 funcargs: bool = False

700 truncate_locals: bool = True

701 chain: bool = True

702 astcache: Dict[Union[str, Path], ast.AST] = attr.ib(

703 factory=dict, init=False, repr=False

704 )

705

706 def _getindent(self, source: "Source") -> int:

707 # Figure out indent for the given source.

708 try:

709 s = str(source.getstatement(len(source) - 1))

710 except KeyboardInterrupt:

711 raise

712 except BaseException:

713 try:

714 s = str(source[-1])

715 except KeyboardInterrupt:

716 raise

717 except BaseException:

718 return 0

719 return 4 + (len(s) - len(s.lstrip()))

720

721 def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:

722 source = entry.getsource(self.astcache)

723 if source is not None:

724 source = source.deindent()

725 return source

726

727 def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:

728 if self.funcargs:

729 args = []

730 for argname, argvalue in entry.frame.getargs(var=True):

731 args.append((argname, saferepr(argvalue)))

732 return ReprFuncArgs(args)

733 return None

734

735 def get_source(

736 self,

737 source: Optional["Source"],

738 line_index: int = -1,

739 excinfo: Optional[ExceptionInfo[BaseException]] = None,

740 short: bool = False,

741 ) -> List[str]:

742 """Return formatted and marked up source lines."""

743 lines = []

744 if source is None or line_index >= len(source.lines):

745 source = Source("???")

746 line_index = 0

747 if line_index < 0:

748 line_index += len(source)

749 space_prefix = " "

750 if short:

751 lines.append(space_prefix + source.lines[line_index].strip())

752 else:

753 for line in source.lines[:line_index]:

754 lines.append(space_prefix + line)

755 lines.append(self.flow_marker + " " + source.lines[line_index])

756 for line in source.lines[line_index + 1 :]:

757 lines.append(space_prefix + line)

758 if excinfo is not None:

759 indent = 4 if short else self._getindent(source)

760 lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))

761 return lines

762

763 def get_exconly(

764 self,

765 excinfo: ExceptionInfo[BaseException],

766 indent: int = 4,

767 markall: bool = False,

768 ) -> List[str]:

769 lines = []

770 indentstr = " " * indent

771 # Get the real exception information out.

772 exlines = excinfo.exconly(tryshort=True).split("\n")

773 failindent = self.fail_marker + indentstr[1:]

774 for line in exlines:

775 lines.append(failindent + line)

776 if not markall:

777 failindent = indentstr

778 return lines

779

780 def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:

781 if self.showlocals:

782 lines = []

783 keys = [loc for loc in locals if loc[0] != "@"]

784 keys.sort()

785 for name in keys:

786 value = locals[name]

787 if name == "__builtins__":

788 lines.append("__builtins__ = <builtins>")

789 else:

790 # This formatting could all be handled by the

791 # _repr() function, which is only reprlib.Repr in

792 # disguise, so is very configurable.

793 if self.truncate_locals:

794 str_repr = saferepr(value)

795 else:

796 str_repr = safeformat(value)

797 # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):

798 lines.append(f"{name:<10} = {str_repr}")

799 # else:

800 # self._line("%-10s =\\" % (name,))

801 # # XXX

802 # pprint.pprint(value, stream=self.excinfowriter)

803 return ReprLocals(lines)

804 return None

805

806 def repr_traceback_entry(

807 self,

808 entry: TracebackEntry,

809 excinfo: Optional[ExceptionInfo[BaseException]] = None,

810 ) -> "ReprEntry":

811 lines: List[str] = []

812 style = entry._repr_style if entry._repr_style is not None else self.style

813 if style in ("short", "long"):

814 source = self._getentrysource(entry)

815 if source is None:

816 source = Source("???")

817 line_index = 0

818 else:

819 line_index = entry.lineno - entry.getfirstlinesource()

820 short = style == "short"

821 reprargs = self.repr_args(entry) if not short else None

822 s = self.get_source(source, line_index, excinfo, short=short)

823 lines.extend(s)

824 if short:

825 message = "in %s" % (entry.name)

826 else:

827 message = excinfo and excinfo.typename or ""

828 entry_path = entry.path

829 path = self._makepath(entry_path)

830 reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)

831 localsrepr = self.repr_locals(entry.locals)

832 return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)

833 elif style == "value":

834 if excinfo:

835 lines.extend(str(excinfo.value).split("\n"))

836 return ReprEntry(lines, None, None, None, style)

837 else:

838 if excinfo:

839 lines.extend(self.get_exconly(excinfo, indent=4))

840 return ReprEntry(lines, None, None, None, style)

841

842 def _makepath(self, path: Union[Path, str]) -> str:

843 if not self.abspath and isinstance(path, Path):

844 try:

845 np = bestrelpath(Path.cwd(), path)

846 except OSError:

847 return str(path)

848 if len(np) < len(str(path)):

849 return np

850 return str(path)

851

852 def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":

853 traceback = excinfo.traceback

854 if self.tbfilter:

855 traceback = traceback.filter()

856

857 if isinstance(excinfo.value, RecursionError):

858 traceback, extraline = self._truncate_recursive_traceback(traceback)

859 else:

860 extraline = None

861

862 last = traceback[-1]

863 entries = []

864 if self.style == "value":

865 reprentry = self.repr_traceback_entry(last, excinfo)

866 entries.append(reprentry)

867 return ReprTraceback(entries, None, style=self.style)

868

869 for index, entry in enumerate(traceback):

870 einfo = (last == entry) and excinfo or None

871 reprentry = self.repr_traceback_entry(entry, einfo)

872 entries.append(reprentry)

873 return ReprTraceback(entries, extraline, style=self.style)

874

875 def _truncate_recursive_traceback(

876 self, traceback: Traceback

877 ) -> Tuple[Traceback, Optional[str]]:

878 """Truncate the given recursive traceback trying to find the starting

879 point of the recursion.

880

881 The detection is done by going through each traceback entry and

882 finding the point in which the locals of the frame are equal to the

883 locals of a previous frame (see ``recursionindex()``).

884

885 Handle the situation where the recursion process might raise an

886 exception (for example comparing numpy arrays using equality raises a

887 TypeError), in which case we do our best to warn the user of the

888 error and show a limited traceback.

889 """

890 try:

891 recursionindex = traceback.recursionindex()

892 except Exception as e:

893 max_frames = 10

894 extraline: Optional[str] = (

895 "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"

896 " The following exception happened when comparing locals in the stack frame:\n"

897 " {exc_type}: {exc_msg}\n"

898 " Displaying first and last {max_frames} stack frames out of {total}."

899 ).format(

900 exc_type=type(e).__name__,

901 exc_msg=str(e),

902 max_frames=max_frames,

903 total=len(traceback),

904 )

905 # Type ignored because adding two instances of a List subtype

906 # currently incorrectly has type List instead of the subtype.

907 traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore

908 else:

909 if recursionindex is not None:

910 extraline = "!!! Recursion detected (same locals & position)"

911 traceback = traceback[: recursionindex + 1]

912 else:

913 extraline = None

914

915 return traceback, extraline

916

917 def repr_excinfo(

918 self, excinfo: ExceptionInfo[BaseException]

919 ) -> "ExceptionChainRepr":

920 repr_chain: List[

921 Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]

922 ] = []

923 e: Optional[BaseException] = excinfo.value

924 excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo

925 descr = None

926 seen: Set[int] = set()

927 while e is not None and id(e) not in seen:

928 seen.add(id(e))

929 if excinfo_:

930 # Fall back to native traceback as a temporary workaround until

931 # full support for exception groups added to ExceptionInfo.

932 # See https://github.com/pytest-dev/pytest/issues/9159

933 if isinstance(e, BaseExceptionGroup):

934 reprtraceback: Union[

935 ReprTracebackNative, ReprTraceback

936 ] = ReprTracebackNative(

937 traceback.format_exception(

938 type(excinfo_.value),

939 excinfo_.value,

940 excinfo_.traceback[0]._rawentry,

941 )

942 )

943 else:

944 reprtraceback = self.repr_traceback(excinfo_)

945 reprcrash: Optional[ReprFileLocation] = (

946 excinfo_._getreprcrash() if self.style != "value" else None

947 )

948 else:

949 # Fallback to native repr if the exception doesn't have a traceback:

950 # ExceptionInfo objects require a full traceback to work.

951 reprtraceback = ReprTracebackNative(

952 traceback.format_exception(type(e), e, None)

953 )

954 reprcrash = None

955

956 repr_chain += [(reprtraceback, reprcrash, descr)]

957 if e.__cause__ is not None and self.chain:

958 e = e.__cause__

959 excinfo_ = (

960 ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))

961 if e.__traceback__

962 else None

963 )

964 descr = "The above exception was the direct cause of the following exception:"

965 elif (

966 e.__context__ is not None and not e.__suppress_context__ and self.chain

967 ):

968 e = e.__context__

969 excinfo_ = (

970 ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))

971 if e.__traceback__

972 else None

973 )

974 descr = "During handling of the above exception, another exception occurred:"

975 else:

976 e = None

977 repr_chain.reverse()

978 return ExceptionChainRepr(repr_chain)

979

980

981@attr.s(eq=False, auto_attribs=True)

982 class TerminalRepr:

983 def __str__(self) -> str:

984 # FYI this is called from pytest-xdist's serialization of exception

985 # information.

986 io = StringIO()

987 tw = TerminalWriter(file=io)

988 self.toterminal(tw)

989 return io.getvalue().strip()

990

991 def __repr__(self) -> str:

992 return f"<{self.__class__} instance at {id(self):0x}>"

993

994 def toterminal(self, tw: TerminalWriter) -> None:

995 raise NotImplementedError()

996

997

998# This class is abstract -- only subclasses are instantiated.

999@attr.s(eq=False)

1000 class ExceptionRepr(TerminalRepr):

1001 # Provided by subclasses.

1002 reprcrash: Optional["ReprFileLocation"]

1003 reprtraceback: "ReprTraceback"

1004

1005 def __attrs_post_init__(self) -> None:

1006 self.sections: List[Tuple[str, str, str]] = []

1007

1008 def addsection(self, name: str, content: str, sep: str = "-") -> None:

1009 self.sections.append((name, content, sep))

1010

1011 def toterminal(self, tw: TerminalWriter) -> None:

1012 for name, content, sep in self.sections:

1013 tw.sep(sep, name)

1014 tw.line(content)

1015

1016

1017@attr.s(eq=False, auto_attribs=True)

1018 class ExceptionChainRepr(ExceptionRepr):

1019 chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]

1020

1021 def __attrs_post_init__(self) -> None:

1022 super().__attrs_post_init__()

1023 # reprcrash and reprtraceback of the outermost (the newest) exception

1024 # in the chain.

1025 self.reprtraceback = self.chain[-1][0]

1026 self.reprcrash = self.chain[-1][1]

1027

1028 def toterminal(self, tw: TerminalWriter) -> None:

1029 for element in self.chain:

1030 element[0].toterminal(tw)

1031 if element[2] is not None:

1032 tw.line("")

1033 tw.line(element[2], yellow=True)

1034 super().toterminal(tw)

1035

1036

1037@attr.s(eq=False, auto_attribs=True)

1038 class ReprExceptionInfo(ExceptionRepr):

1039 reprtraceback: "ReprTraceback"

1040 reprcrash: "ReprFileLocation"

1041

1042 def toterminal(self, tw: TerminalWriter) -> None:

1043 self.reprtraceback.toterminal(tw)

1044 super().toterminal(tw)

1045

1046

1047@attr.s(eq=False, auto_attribs=True)

1048 class ReprTraceback(TerminalRepr):

1049 reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]

1050 extraline: Optional[str]

1051 style: "_TracebackStyle"

1052

1053 entrysep: ClassVar = "_ "

1054

1055 def toterminal(self, tw: TerminalWriter) -> None:

1056 # The entries might have different styles.

1057 for i, entry in enumerate(self.reprentries):

1058 if entry.style == "long":

1059 tw.line("")

1060 entry.toterminal(tw)

1061 if i < len(self.reprentries) - 1:

1062 next_entry = self.reprentries[i + 1]

1063 if (

1064 entry.style == "long"

1065 or entry.style == "short"

1066 and next_entry.style == "long"

1067 ):

1068 tw.sep(self.entrysep)

1069

1070 if self.extraline:

1071 tw.line(self.extraline)

1072

1073

1074 class ReprTracebackNative(ReprTraceback):

1075 def __init__(self, tblines: Sequence[str]) -> None:

1076 self.style = "native"

1077 self.reprentries = [ReprEntryNative(tblines)]

1078 self.extraline = None

1079

1080

1081@attr.s(eq=False, auto_attribs=True)

1082 class ReprEntryNative(TerminalRepr):

1083 lines: Sequence[str]

1084

1085 style: ClassVar["_TracebackStyle"] = "native"

1086

1087 def toterminal(self, tw: TerminalWriter) -> None:

1088 tw.write("".join(self.lines))

1089

1090

1091@attr.s(eq=False, auto_attribs=True)

1092 class ReprEntry(TerminalRepr):

1093 lines: Sequence[str]

1094 reprfuncargs: Optional["ReprFuncArgs"]

1095 reprlocals: Optional["ReprLocals"]

1096 reprfileloc: Optional["ReprFileLocation"]

1097 style: "_TracebackStyle"

1098

1099 def _write_entry_lines(self, tw: TerminalWriter) -> None:

1100 """Write the source code portions of a list of traceback entries with syntax highlighting.

1101

1102 Usually entries are lines like these:

1103

1104 " x = 1"

1105 "> assert x == 2"

1106 "E assert 1 == 2"

1107

1108 This function takes care of rendering the "source" portions of it (the lines without

1109 the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"

1110 character, as doing so might break line continuations.

1111 """

1112

1113 if not self.lines:

1114 return

1115

1116 # separate indents and source lines that are not failures: we want to

1117 # highlight the code but not the indentation, which may contain markers

1118 # such as "> assert 0"

1119 fail_marker = f"{FormattedExcinfo.fail_marker} "

1120 indent_size = len(fail_marker)

1121 indents: List[str] = []

1122 source_lines: List[str] = []

1123 failure_lines: List[str] = []

1124 for index, line in enumerate(self.lines):

1125 is_failure_line = line.startswith(fail_marker)

1126 if is_failure_line:

1127 # from this point on all lines are considered part of the failure

1128 failure_lines.extend(self.lines[index:])

1129 break

1130 else:

1131 if self.style == "value":

1132 source_lines.append(line)

1133 else:

1134 indents.append(line[:indent_size])

1135 source_lines.append(line[indent_size:])

1136

1137 tw._write_source(source_lines, indents)

1138

1139 # failure lines are always completely red and bold

1140 for line in failure_lines:

1141 tw.line(line, bold=True, red=True)

1142

1143 def toterminal(self, tw: TerminalWriter) -> None:

1144 if self.style == "short":

1145 assert self.reprfileloc is not None

1146 self.reprfileloc.toterminal(tw)

1147 self._write_entry_lines(tw)

1148 if self.reprlocals:

1149 self.reprlocals.toterminal(tw, indent=" " * 8)

1150 return

1151

1152 if self.reprfuncargs:

1153 self.reprfuncargs.toterminal(tw)

1154

1155 self._write_entry_lines(tw)

1156

1157 if self.reprlocals:

1158 tw.line("")

1159 self.reprlocals.toterminal(tw)

1160 if self.reprfileloc:

1161 if self.lines:

1162 tw.line("")

1163 self.reprfileloc.toterminal(tw)

1164

1165 def __str__(self) -> str:

1166 return "{}\n{}\n{}".format(

1167 "\n".join(self.lines), self.reprlocals, self.reprfileloc

1168 )

1169

1170

1171@attr.s(eq=False, auto_attribs=True)

1172 class ReprFileLocation(TerminalRepr):

1173 path: str = attr.ib(converter=str)

1174 lineno: int

1175 message: str

1176

1177 def toterminal(self, tw: TerminalWriter) -> None:

1178 # Filename and lineno output for each entry, using an output format

1179 # that most editors understand.

1180 msg = self.message

1181 i = msg.find("\n")

1182 if i != -1:

1183 msg = msg[:i]

1184 tw.write(self.path, bold=True, red=True)

1185 tw.line(f":{self.lineno}: {msg}")

1186

1187

1188@attr.s(eq=False, auto_attribs=True)

1189 class ReprLocals(TerminalRepr):

1190 lines: Sequence[str]

1191

1192 def toterminal(self, tw: TerminalWriter, indent="") -> None:

1193 for line in self.lines:

1194 tw.line(indent + line)

1195

1196

1197@attr.s(eq=False, auto_attribs=True)

1198 class ReprFuncArgs(TerminalRepr):

1199 args: Sequence[Tuple[str, object]]

1200

1201 def toterminal(self, tw: TerminalWriter) -> None:

1202 if self.args:

1203 linesofar = ""

1204 for name, value in self.args:

1205 ns = f"{name} = {value}"

1206 if len(ns) + len(linesofar) + 2 > tw.fullwidth:

1207 if linesofar:

1208 tw.line(linesofar)

1209 linesofar = ns

1210 else:

1211 if linesofar:

1212 linesofar += ", " + ns

1213 else:

1214 linesofar = ns

1215 if linesofar:

1216 tw.line(linesofar)

1217 tw.line("")

1218

1219

1220 def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:

1221 """Return source location (path, lineno) for the given object.

1222

1223 If the source cannot be determined return ("", -1).

1224

1225 The line number is 0-based.

1226 """

1227 # xxx let decorators etc specify a sane ordering

1228 # NOTE: this used to be done in _pytest.compat.getfslineno, initially added

1229 # in 6ec13a2b9. It ("place_as") appears to be something very custom.

1230 obj = get_real_func(obj)

1231 if hasattr(obj, "place_as"):

1232 obj = obj.place_as # type: ignore[attr-defined]

1233

1234 try:

1235 code = Code.from_function(obj)

1236 except TypeError:

1237 try:

1238 fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]

1239 except TypeError:

1240 return "", -1

1241

1242 fspath = fn and absolutepath(fn) or ""

1243 lineno = -1

1244 if fspath:

1245 try:

1246 _, lineno = findsource(obj)

1247 except OSError:

1248 pass

1249 return fspath, lineno

1250

1251 return code.path, code.firstlineno

1252

1253

1254# Relative paths that we use to filter traceback entries from appearing to the user;

1255# see filter_traceback.

1256# note: if we need to add more paths than what we have now we should probably use a list

1257# for better maintenance.

1258

1259_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))

1260# pluggy is either a package or a single module depending on the version

1261 if _PLUGGY_DIR.name == "__init__.py":

1262 _PLUGGY_DIR = _PLUGGY_DIR.parent

1263_PYTEST_DIR = Path(_pytest.__file__).parent

1264

1265

1266 def filter_traceback(entry: TracebackEntry) -> bool:

1267 """Return True if a TracebackEntry instance should be included in tracebacks.

1268

1269 We hide traceback entries of:

1270

1271 * dynamically generated code (no code to show up for it);

1272 * internal traceback from pytest or its internal libraries, py and pluggy.

1273 """

1274 # entry.path might sometimes return a str object when the entry

1275 # points to dynamically generated code.

1276 # See https://bitbucket.org/pytest-dev/py/issues/71.

1277 raw_filename = entry.frame.code.raw.co_filename

1278 is_generated = "<" in raw_filename and ">" in raw_filename

1279 if is_generated:

1280 return False

1281

1282 # entry.path might point to a non-existing file, in which case it will

1283 # also return a str object. See #1133.

1284 p = Path(entry.path)

1285

1286 parents = p.parents

1287 if _PLUGGY_DIR in parents:

1288 return False

1289 if _PYTEST_DIR in parents:

1290 return False

1291

1292 return True