Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/_code/code.py: 36%

721 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1import ast 

2import inspect 

3import os 

4import re 

5import sys 

6import traceback 

7from inspect import CO_VARARGS 

8from inspect import CO_VARKEYWORDS 

9from io import StringIO 

10from pathlib import Path 

11from traceback import format_exception_only 

12from types import CodeType 

13from types import FrameType 

14from types import TracebackType 

15from typing import Any 

16from typing import Callable 

17from typing import ClassVar 

18from typing import Dict 

19from typing import Generic 

20from typing import Iterable 

21from typing import List 

22from typing import Mapping 

23from typing import Optional 

24from typing import overload 

25from typing import Pattern 

26from typing import Sequence 

27from typing import Set 

28from typing import Tuple 

29from typing import Type 

30from typing import TYPE_CHECKING 

31from typing import TypeVar 

32from typing import Union 

33from weakref import ref 

34 

35import attr 

36import pluggy 

37 

38import _pytest 

39from _pytest._code.source import findsource 

40from _pytest._code.source import getrawcode 

41from _pytest._code.source import getstatementrange_ast 

42from _pytest._code.source import Source 

43from _pytest._io import TerminalWriter 

44from _pytest._io.saferepr import safeformat 

45from _pytest._io.saferepr import saferepr 

46from _pytest.compat import final 

47from _pytest.compat import get_real_func 

48from _pytest.deprecated import check_ispytest 

49from _pytest.pathlib import absolutepath 

50from _pytest.pathlib import bestrelpath 

51 

52if 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 

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

60 from exceptiongroup import BaseExceptionGroup 

61 

62 

63class 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 

132class 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 

194class 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 

326class 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 

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

445 

446 

447@final 

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

449class ExceptionInfo(Generic[E]): 

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

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) 

688class 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) 

982class TerminalRepr: 

983 def __str__(self) -> str: 

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

985 # information. 

986 io = StringIO() 

987 tw = TerminalWriter(file=io) 

988 self.toterminal(tw) 

989 return io.getvalue().strip() 

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) 

1000class 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) 

1018class 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) 

1038class 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) 

1048class 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 

1074class 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) 

1082class 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) 

1092class 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) 

1172class 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) 

1189class 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) 

1198class 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 

1220def 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 

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

1262 _PLUGGY_DIR = _PLUGGY_DIR.parent 

1263_PYTEST_DIR = Path(_pytest.__file__).parent 

1264 

1265 

1266def 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