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
« 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
7import collections.abc
8import sys
9import textwrap
10import traceback
11from functools import singledispatch
12from types import TracebackType
13from typing import Any, List, Optional
15from ._exceptions import BaseExceptionGroup
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)
23_context_message = (
24 "\nDuring handling of the above exception, another exception occurred:\n\n"
25)
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"
35 return line
38def _safe_string(value, what, func=str):
39 try:
40 return func(value)
41 except BaseException:
42 return f"<{what} {func.__name__}() failed>"
45class _ExceptionPrintContext:
46 def __init__(self):
47 self.seen = set()
48 self.exception_group_depth = 0
49 self.need_close = False
51 def indent(self):
52 return " " * (2 * self.exception_group_depth)
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 + " "
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)
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)))
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
91 is_recursive_call = _seen is not None
92 if _seen is None:
93 _seen = set()
94 _seen.add(id(exc_value))
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
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}'?"
138 if lookup_lines:
139 # Force all lines in the stack to be loaded
140 for frame in self.stack:
141 frame.line
143 self.__suppress_context__ = (
144 exc_value.__suppress_context__ if exc_value is not None else False
145 )
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()
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
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
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
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))
218 def format(self, *, chain=True, _ctx=None):
219 if _ctx is None:
220 _ctx = _ExceptionPrintContext()
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
236 output.append((chained_msg, exc))
237 exc = chained_exc
238 else:
239 output.append((None, exc))
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
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())
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
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 )
298 if last_exc and _ctx.need_close:
299 yield _ctx.indent() + "+------------------------------------\n"
300 _ctx.need_close = False
301 _ctx.exception_group_depth -= 1
303 if is_toplevel:
304 assert _ctx.exception_group_depth == 1
305 _ctx.exception_group_depth = 0
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
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
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)
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)
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
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 )
372@format_exception_only.register
373def _(__exc: type, value: BaseException) -> List[str]:
374 return format_exception_only(value)
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 )
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)
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
411 for line in PatchedTracebackException(
412 type(__exc), __exc, __exc.__traceback__, limit=limit
413 ).format(chain=chain):
414 print(line, file=file, end="")
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)
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)
438# Python levenshtein edit distance code for NameError/AttributeError
439# suggestions, backported from 3.12
441_MAX_CANDIDATE_ITEMS = 750
442_MAX_STRING_SIZE = 40
443_MOVE_COST = 2
444_CASE_COST = 1
445_SENTINEL = object()
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
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
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
505def _levenshtein_distance(a, b, max_cost):
506 # A Python implementation of Python/suggestions.c:levenshtein_distance.
508 # Both strings are the same
509 if a == b:
510 return 0
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
528 # Prefer shorter buffer
529 if len(b) < len(a):
530 a, b = b, a
532 # Quick fail when a match is impossible
533 if (len(b) - len(a)) * _MOVE_COST > max_cost:
534 return max_cost + 1
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))
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])
553 insert_delete = min(result, distance) + _MOVE_COST
554 result = min(insert_delete, substitute)
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