Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/exceptiongroup/_formatting.py: 17%

312 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:05 +0000

1# traceback_exception_init() adapted from trio 

2# 

3# _ExceptionPrintContext and traceback_exception_format() copied from the standard 

4# library 

5from __future__ import annotations 

6 

7import collections.abc 

8import sys 

9import textwrap 

10import traceback 

11from functools import singledispatch 

12from types import TracebackType 

13from typing import Any, List, Optional 

14 

15from ._exceptions import BaseExceptionGroup 

16 

17max_group_width = 15 

18max_group_depth = 10 

19_cause_message = ( 

20 "\nThe above exception was the direct cause of the following exception:\n\n" 

21) 

22 

23_context_message = ( 

24 "\nDuring handling of the above exception, another exception occurred:\n\n" 

25) 

26 

27 

28def _format_final_exc_line(etype, value): 

29 valuestr = _safe_string(value, "exception") 

30 if value is None or not valuestr: 

31 line = f"{etype}\n" 

32 else: 

33 line = f"{etype}: {valuestr}\n" 

34 

35 return line 

36 

37 

38def _safe_string(value, what, func=str): 

39 try: 

40 return func(value) 

41 except BaseException: 

42 return f"<{what} {func.__name__}() failed>" 

43 

44 

45class _ExceptionPrintContext: 

46 def __init__(self): 

47 self.seen = set() 

48 self.exception_group_depth = 0 

49 self.need_close = False 

50 

51 def indent(self): 

52 return " " * (2 * self.exception_group_depth) 

53 

54 def emit(self, text_gen, margin_char=None): 

55 if margin_char is None: 

56 margin_char = "|" 

57 indent_str = self.indent() 

58 if self.exception_group_depth: 

59 indent_str += margin_char + " " 

60 

61 if isinstance(text_gen, str): 

62 yield textwrap.indent(text_gen, indent_str, lambda line: True) 

63 else: 

64 for text in text_gen: 

65 yield textwrap.indent(text, indent_str, lambda line: True) 

66 

67 

68def exceptiongroup_excepthook( 

69 etype: type[BaseException], value: BaseException, tb: TracebackType | None 

70) -> None: 

71 sys.stderr.write("".join(traceback.format_exception(etype, value, tb))) 

72 

73 

74class PatchedTracebackException(traceback.TracebackException): 

75 def __init__( 

76 self, 

77 exc_type: type[BaseException], 

78 exc_value: BaseException, 

79 exc_traceback: TracebackType | None, 

80 *, 

81 limit: int | None = None, 

82 lookup_lines: bool = True, 

83 capture_locals: bool = False, 

84 compact: bool = False, 

85 _seen: set[int] | None = None, 

86 ) -> None: 

87 kwargs: dict[str, Any] = {} 

88 if sys.version_info >= (3, 10): 

89 kwargs["compact"] = compact 

90 

91 is_recursive_call = _seen is not None 

92 if _seen is None: 

93 _seen = set() 

94 _seen.add(id(exc_value)) 

95 

96 self.stack = traceback.StackSummary.extract( 

97 traceback.walk_tb(exc_traceback), 

98 limit=limit, 

99 lookup_lines=lookup_lines, 

100 capture_locals=capture_locals, 

101 ) 

102 self.exc_type = exc_type 

103 # Capture now to permit freeing resources: only complication is in the 

104 # unofficial API _format_final_exc_line 

105 self._str = _safe_string(exc_value, "exception") 

106 try: 

107 self.__notes__ = getattr(exc_value, "__notes__", None) 

108 except KeyError: 

109 # Workaround for https://github.com/python/cpython/issues/98778 on Python 

110 # <= 3.9, and some 3.10 and 3.11 patch versions. 

111 HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) 

112 if sys.version_info[:2] <= (3, 11) and isinstance(exc_value, HTTPError): 

113 self.__notes__ = None 

114 else: 

115 raise 

116 

117 if exc_type and issubclass(exc_type, SyntaxError): 

118 # Handle SyntaxError's specially 

119 self.filename = exc_value.filename 

120 lno = exc_value.lineno 

121 self.lineno = str(lno) if lno is not None else None 

122 self.text = exc_value.text 

123 self.offset = exc_value.offset 

124 self.msg = exc_value.msg 

125 if sys.version_info >= (3, 10): 

126 end_lno = exc_value.end_lineno 

127 self.end_lineno = str(end_lno) if end_lno is not None else None 

128 self.end_offset = exc_value.end_offset 

129 elif ( 

130 exc_type 

131 and issubclass(exc_type, (NameError, AttributeError)) 

132 and getattr(exc_value, "name", None) is not None 

133 ): 

134 suggestion = _compute_suggestion_error(exc_value, exc_traceback) 

135 if suggestion: 

136 self._str += f". Did you mean: '{suggestion}'?" 

137 

138 if lookup_lines: 

139 # Force all lines in the stack to be loaded 

140 for frame in self.stack: 

141 frame.line 

142 

143 self.__suppress_context__ = ( 

144 exc_value.__suppress_context__ if exc_value is not None else False 

145 ) 

146 

147 # Convert __cause__ and __context__ to `TracebackExceptions`s, use a 

148 # queue to avoid recursion (only the top-level call gets _seen == None) 

149 if not is_recursive_call: 

150 queue = [(self, exc_value)] 

151 while queue: 

152 te, e = queue.pop() 

153 

154 if e and e.__cause__ is not None and id(e.__cause__) not in _seen: 

155 cause = PatchedTracebackException( 

156 type(e.__cause__), 

157 e.__cause__, 

158 e.__cause__.__traceback__, 

159 limit=limit, 

160 lookup_lines=lookup_lines, 

161 capture_locals=capture_locals, 

162 _seen=_seen, 

163 ) 

164 else: 

165 cause = None 

166 

167 if compact: 

168 need_context = ( 

169 cause is None and e is not None and not e.__suppress_context__ 

170 ) 

171 else: 

172 need_context = True 

173 if ( 

174 e 

175 and e.__context__ is not None 

176 and need_context 

177 and id(e.__context__) not in _seen 

178 ): 

179 context = PatchedTracebackException( 

180 type(e.__context__), 

181 e.__context__, 

182 e.__context__.__traceback__, 

183 limit=limit, 

184 lookup_lines=lookup_lines, 

185 capture_locals=capture_locals, 

186 _seen=_seen, 

187 ) 

188 else: 

189 context = None 

190 

191 # Capture each of the exceptions in the ExceptionGroup along with each 

192 # of their causes and contexts 

193 if e and isinstance(e, BaseExceptionGroup): 

194 exceptions = [] 

195 for exc in e.exceptions: 

196 texc = PatchedTracebackException( 

197 type(exc), 

198 exc, 

199 exc.__traceback__, 

200 lookup_lines=lookup_lines, 

201 capture_locals=capture_locals, 

202 _seen=_seen, 

203 ) 

204 exceptions.append(texc) 

205 else: 

206 exceptions = None 

207 

208 te.__cause__ = cause 

209 te.__context__ = context 

210 te.exceptions = exceptions 

211 if cause: 

212 queue.append((te.__cause__, e.__cause__)) 

213 if context: 

214 queue.append((te.__context__, e.__context__)) 

215 if exceptions: 

216 queue.extend(zip(te.exceptions, e.exceptions)) 

217 

218 def format(self, *, chain=True, _ctx=None): 

219 if _ctx is None: 

220 _ctx = _ExceptionPrintContext() 

221 

222 output = [] 

223 exc = self 

224 if chain: 

225 while exc: 

226 if exc.__cause__ is not None: 

227 chained_msg = _cause_message 

228 chained_exc = exc.__cause__ 

229 elif exc.__context__ is not None and not exc.__suppress_context__: 

230 chained_msg = _context_message 

231 chained_exc = exc.__context__ 

232 else: 

233 chained_msg = None 

234 chained_exc = None 

235 

236 output.append((chained_msg, exc)) 

237 exc = chained_exc 

238 else: 

239 output.append((None, exc)) 

240 

241 for msg, exc in reversed(output): 

242 if msg is not None: 

243 yield from _ctx.emit(msg) 

244 if exc.exceptions is None: 

245 if exc.stack: 

246 yield from _ctx.emit("Traceback (most recent call last):\n") 

247 yield from _ctx.emit(exc.stack.format()) 

248 yield from _ctx.emit(exc.format_exception_only()) 

249 elif _ctx.exception_group_depth > max_group_depth: 

250 # exception group, but depth exceeds limit 

251 yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n") 

252 else: 

253 # format exception group 

254 is_toplevel = _ctx.exception_group_depth == 0 

255 if is_toplevel: 

256 _ctx.exception_group_depth += 1 

257 

258 if exc.stack: 

259 yield from _ctx.emit( 

260 "Exception Group Traceback (most recent call last):\n", 

261 margin_char="+" if is_toplevel else None, 

262 ) 

263 yield from _ctx.emit(exc.stack.format()) 

264 

265 yield from _ctx.emit(exc.format_exception_only()) 

266 num_excs = len(exc.exceptions) 

267 if num_excs <= max_group_width: 

268 n = num_excs 

269 else: 

270 n = max_group_width + 1 

271 _ctx.need_close = False 

272 for i in range(n): 

273 last_exc = i == n - 1 

274 if last_exc: 

275 # The closing frame may be added by a recursive call 

276 _ctx.need_close = True 

277 

278 if max_group_width is not None: 

279 truncated = i >= max_group_width 

280 else: 

281 truncated = False 

282 title = f"{i + 1}" if not truncated else "..." 

283 yield ( 

284 _ctx.indent() 

285 + ("+-" if i == 0 else " ") 

286 + f"+---------------- {title} ----------------\n" 

287 ) 

288 _ctx.exception_group_depth += 1 

289 if not truncated: 

290 yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx) 

291 else: 

292 remaining = num_excs - max_group_width 

293 plural = "s" if remaining > 1 else "" 

294 yield from _ctx.emit( 

295 f"and {remaining} more exception{plural}\n" 

296 ) 

297 

298 if last_exc and _ctx.need_close: 

299 yield _ctx.indent() + "+------------------------------------\n" 

300 _ctx.need_close = False 

301 _ctx.exception_group_depth -= 1 

302 

303 if is_toplevel: 

304 assert _ctx.exception_group_depth == 1 

305 _ctx.exception_group_depth = 0 

306 

307 def format_exception_only(self): 

308 """Format the exception part of the traceback. 

309 The return value is a generator of strings, each ending in a newline. 

310 Normally, the generator emits a single string; however, for 

311 SyntaxError exceptions, it emits several lines that (when 

312 printed) display detailed information about where the syntax 

313 error occurred. 

314 The message indicating which exception occurred is always the last 

315 string in the output. 

316 """ 

317 if self.exc_type is None: 

318 yield traceback._format_final_exc_line(None, self._str) 

319 return 

320 

321 stype = self.exc_type.__qualname__ 

322 smod = self.exc_type.__module__ 

323 if smod not in ("__main__", "builtins"): 

324 if not isinstance(smod, str): 

325 smod = "<unknown>" 

326 stype = smod + "." + stype 

327 

328 if not issubclass(self.exc_type, SyntaxError): 

329 yield _format_final_exc_line(stype, self._str) 

330 elif traceback_exception_format_syntax_error is not None: 

331 yield from traceback_exception_format_syntax_error(self, stype) 

332 else: 

333 yield from traceback_exception_original_format_exception_only(self) 

334 

335 if isinstance(self.__notes__, collections.abc.Sequence): 

336 for note in self.__notes__: 

337 note = _safe_string(note, "note") 

338 yield from [line + "\n" for line in note.split("\n")] 

339 elif self.__notes__ is not None: 

340 yield _safe_string(self.__notes__, "__notes__", func=repr) 

341 

342 

343traceback_exception_original_format = traceback.TracebackException.format 

344traceback_exception_original_format_exception_only = ( 

345 traceback.TracebackException.format_exception_only 

346) 

347traceback_exception_format_syntax_error = getattr( 

348 traceback.TracebackException, "_format_syntax_error", None 

349) 

350if sys.excepthook is sys.__excepthook__: 

351 traceback.TracebackException.__init__ = ( # type: ignore[assignment] 

352 PatchedTracebackException.__init__ 

353 ) 

354 traceback.TracebackException.format = ( # type: ignore[assignment] 

355 PatchedTracebackException.format 

356 ) 

357 traceback.TracebackException.format_exception_only = ( # type: ignore[assignment] 

358 PatchedTracebackException.format_exception_only 

359 ) 

360 sys.excepthook = exceptiongroup_excepthook 

361 

362 

363@singledispatch 

364def format_exception_only(__exc: BaseException) -> List[str]: 

365 return list( 

366 PatchedTracebackException( 

367 type(__exc), __exc, None, compact=True 

368 ).format_exception_only() 

369 ) 

370 

371 

372@format_exception_only.register 

373def _(__exc: type, value: BaseException) -> List[str]: 

374 return format_exception_only(value) 

375 

376 

377@singledispatch 

378def format_exception( 

379 __exc: BaseException, 

380 limit: Optional[int] = None, 

381 chain: bool = True, 

382) -> List[str]: 

383 return list( 

384 PatchedTracebackException( 

385 type(__exc), __exc, __exc.__traceback__, limit=limit, compact=True 

386 ).format(chain=chain) 

387 ) 

388 

389 

390@format_exception.register 

391def _( 

392 __exc: type, 

393 value: BaseException, 

394 tb: TracebackType, 

395 limit: Optional[int] = None, 

396 chain: bool = True, 

397) -> List[str]: 

398 return format_exception(value, limit, chain) 

399 

400 

401@singledispatch 

402def print_exception( 

403 __exc: BaseException, 

404 limit: Optional[int] = None, 

405 file: Any = None, 

406 chain: bool = True, 

407) -> None: 

408 if file is None: 

409 file = sys.stderr 

410 

411 for line in PatchedTracebackException( 

412 type(__exc), __exc, __exc.__traceback__, limit=limit 

413 ).format(chain=chain): 

414 print(line, file=file, end="") 

415 

416 

417@print_exception.register 

418def _( 

419 __exc: type, 

420 value: BaseException, 

421 tb: TracebackType, 

422 limit: Optional[int] = None, 

423 file: Any = None, 

424 chain: bool = True, 

425) -> None: 

426 print_exception(value, limit, file, chain) 

427 

428 

429def print_exc( 

430 limit: Optional[int] = None, 

431 file: Any | None = None, 

432 chain: bool = True, 

433) -> None: 

434 value = sys.exc_info()[1] 

435 print_exception(value, limit, file, chain) 

436 

437 

438# Python levenshtein edit distance code for NameError/AttributeError 

439# suggestions, backported from 3.12 

440 

441_MAX_CANDIDATE_ITEMS = 750 

442_MAX_STRING_SIZE = 40 

443_MOVE_COST = 2 

444_CASE_COST = 1 

445_SENTINEL = object() 

446 

447 

448def _substitution_cost(ch_a, ch_b): 

449 if ch_a == ch_b: 

450 return 0 

451 if ch_a.lower() == ch_b.lower(): 

452 return _CASE_COST 

453 return _MOVE_COST 

454 

455 

456def _compute_suggestion_error(exc_value, tb): 

457 wrong_name = getattr(exc_value, "name", None) 

458 if wrong_name is None or not isinstance(wrong_name, str): 

459 return None 

460 if isinstance(exc_value, AttributeError): 

461 obj = getattr(exc_value, "obj", _SENTINEL) 

462 if obj is _SENTINEL: 

463 return None 

464 obj = exc_value.obj 

465 try: 

466 d = dir(obj) 

467 except Exception: 

468 return None 

469 else: 

470 assert isinstance(exc_value, NameError) 

471 # find most recent frame 

472 if tb is None: 

473 return None 

474 while tb.tb_next is not None: 

475 tb = tb.tb_next 

476 frame = tb.tb_frame 

477 

478 d = list(frame.f_locals) + list(frame.f_globals) + list(frame.f_builtins) 

479 if len(d) > _MAX_CANDIDATE_ITEMS: 

480 return None 

481 wrong_name_len = len(wrong_name) 

482 if wrong_name_len > _MAX_STRING_SIZE: 

483 return None 

484 best_distance = wrong_name_len 

485 suggestion = None 

486 for possible_name in d: 

487 if possible_name == wrong_name: 

488 # A missing attribute is "found". Don't suggest it (see GH-88821). 

489 continue 

490 # No more than 1/3 of the involved characters should need changed. 

491 max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6 

492 # Don't take matches we've already beaten. 

493 max_distance = min(max_distance, best_distance - 1) 

494 current_distance = _levenshtein_distance( 

495 wrong_name, possible_name, max_distance 

496 ) 

497 if current_distance > max_distance: 

498 continue 

499 if not suggestion or current_distance < best_distance: 

500 suggestion = possible_name 

501 best_distance = current_distance 

502 return suggestion 

503 

504 

505def _levenshtein_distance(a, b, max_cost): 

506 # A Python implementation of Python/suggestions.c:levenshtein_distance. 

507 

508 # Both strings are the same 

509 if a == b: 

510 return 0 

511 

512 # Trim away common affixes 

513 pre = 0 

514 while a[pre:] and b[pre:] and a[pre] == b[pre]: 

515 pre += 1 

516 a = a[pre:] 

517 b = b[pre:] 

518 post = 0 

519 while a[: post or None] and b[: post or None] and a[post - 1] == b[post - 1]: 

520 post -= 1 

521 a = a[: post or None] 

522 b = b[: post or None] 

523 if not a or not b: 

524 return _MOVE_COST * (len(a) + len(b)) 

525 if len(a) > _MAX_STRING_SIZE or len(b) > _MAX_STRING_SIZE: 

526 return max_cost + 1 

527 

528 # Prefer shorter buffer 

529 if len(b) < len(a): 

530 a, b = b, a 

531 

532 # Quick fail when a match is impossible 

533 if (len(b) - len(a)) * _MOVE_COST > max_cost: 

534 return max_cost + 1 

535 

536 # Instead of producing the whole traditional len(a)-by-len(b) 

537 # matrix, we can update just one row in place. 

538 # Initialize the buffer row 

539 row = list(range(_MOVE_COST, _MOVE_COST * (len(a) + 1), _MOVE_COST)) 

540 

541 result = 0 

542 for bindex in range(len(b)): 

543 bchar = b[bindex] 

544 distance = result = bindex * _MOVE_COST 

545 minimum = sys.maxsize 

546 for index in range(len(a)): 

547 # 1) Previous distance in this row is cost(b[:b_index], a[:index]) 

548 substitute = distance + _substitution_cost(bchar, a[index]) 

549 # 2) cost(b[:b_index], a[:index+1]) from previous row 

550 distance = row[index] 

551 # 3) existing result is cost(b[:b_index+1], a[index]) 

552 

553 insert_delete = min(result, distance) + _MOVE_COST 

554 result = min(insert_delete, substitute) 

555 

556 # cost(b[:b_index+1], a[:index+1]) 

557 row[index] = result 

558 if result < minimum: 

559 minimum = result 

560 if minimum > max_cost: 

561 # Everything in this row is too big, so bail early. 

562 return max_cost + 1 

563 return result