Skip to content

Commit 8f5a00b

Browse files
committed
update Lib/traceback.py from CPython v3.10.5
1 parent 5983a5a commit 8f5a00b

File tree

1 file changed

+171
-90
lines changed

1 file changed

+171
-90
lines changed

Lib/traceback.py

Lines changed: 171 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,25 @@ def extract_tb(tb, limit=None):
8484
"another exception occurred:\n\n")
8585

8686

87-
def print_exception(etype, value, tb, limit=None, file=None, chain=True):
87+
class _Sentinel:
88+
def __repr__(self):
89+
return "<implicit>"
90+
91+
_sentinel = _Sentinel()
92+
93+
def _parse_value_tb(exc, value, tb):
94+
if (value is _sentinel) != (tb is _sentinel):
95+
raise ValueError("Both or neither of value and tb must be given")
96+
if value is tb is _sentinel:
97+
if exc is not None:
98+
return exc, exc.__traceback__
99+
else:
100+
return None, None
101+
return value, tb
102+
103+
104+
def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
105+
file=None, chain=True):
88106
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
89107
90108
This differs from print_tb() in the following ways: (1) if
@@ -95,17 +113,16 @@ def print_exception(etype, value, tb, limit=None, file=None, chain=True):
95113
occurred with a caret on the next line indicating the approximate
96114
position of the error.
97115
"""
98-
# format_exception has ignored etype for some time, and code such as cgitb
99-
# passes in bogus values as a result. For compatibility with such code we
100-
# ignore it here (rather than in the new TracebackException API).
116+
value, tb = _parse_value_tb(exc, value, tb)
101117
if file is None:
102118
file = sys.stderr
103-
for line in TracebackException(
104-
type(value), value, tb, limit=limit).format(chain=chain):
119+
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
120+
for line in te.format(chain=chain):
105121
print(line, file=file, end="")
106122

107123

108-
def format_exception(etype, value, tb, limit=None, chain=True):
124+
def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
125+
chain=True):
109126
"""Format a stack trace and the exception information.
110127
111128
The arguments have the same meaning as the corresponding arguments
@@ -114,19 +131,15 @@ def format_exception(etype, value, tb, limit=None, chain=True):
114131
these lines are concatenated and printed, exactly the same text is
115132
printed as does print_exception().
116133
"""
117-
# format_exception has ignored etype for some time, and code such as cgitb
118-
# passes in bogus values as a result. For compatibility with such code we
119-
# ignore it here (rather than in the new TracebackException API).
120-
return list(TracebackException(
121-
type(value), value, tb, limit=limit).format(chain=chain))
134+
value, tb = _parse_value_tb(exc, value, tb)
135+
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
136+
return list(te.format(chain=chain))
122137

123138

124-
def format_exception_only(etype, value):
139+
def format_exception_only(exc, /, value=_sentinel):
125140
"""Format the exception part of a traceback.
126141
127-
The arguments are the exception type and value such as given by
128-
sys.last_type and sys.last_value. The return value is a list of
129-
strings, each ending in a newline.
142+
The return value is a list of strings, each ending in a newline.
130143
131144
Normally, the list contains a single string; however, for
132145
SyntaxError exceptions, it contains several lines that (when
@@ -137,7 +150,10 @@ def format_exception_only(etype, value):
137150
string in the list.
138151
139152
"""
140-
return list(TracebackException(etype, value, None).format_exception_only())
153+
if value is _sentinel:
154+
value = exc
155+
te = TracebackException(type(value), value, None, compact=True)
156+
return list(te.format_exception_only())
141157

142158

143159
# -- not official API but folk probably use these two functions.
@@ -279,12 +295,16 @@ def __repr__(self):
279295
return "<FrameSummary file {filename}, line {lineno} in {name}>".format(
280296
filename=self.filename, lineno=self.lineno, name=self.name)
281297

298+
def __len__(self):
299+
return 4
300+
282301
@property
283302
def line(self):
284303
if self._line is None:
285-
self._line = linecache.getline(self.filename, self.lineno).strip()
286-
return self._line
287-
304+
if self.lineno is None:
305+
return None
306+
self._line = linecache.getline(self.filename, self.lineno)
307+
return self._line.strip()
288308

289309
def walk_stack(f):
290310
"""Walk a stack yielding the frame and line number for each frame.
@@ -455,53 +475,29 @@ class TracebackException:
455475
occurred.
456476
- :attr:`lineno` For syntax errors - the linenumber where the error
457477
occurred.
478+
- :attr:`end_lineno` For syntax errors - the end linenumber where the error
479+
occurred. Can be `None` if not present.
458480
- :attr:`text` For syntax errors - the text where the error
459481
occurred.
460482
- :attr:`offset` For syntax errors - the offset into the text where the
461483
error occurred.
484+
- :attr:`end_offset` For syntax errors - the offset into the text where the
485+
error occurred. Can be `None` if not present.
462486
- :attr:`msg` For syntax errors - the compiler error message.
463487
"""
464488

465489
def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
466-
lookup_lines=True, capture_locals=False, _seen=None):
490+
lookup_lines=True, capture_locals=False, compact=False,
491+
_seen=None):
467492
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
468493
# permit backwards compat with the existing API, otherwise we
469494
# need stub thunk objects just to glue it together.
470495
# Handle loops in __cause__ or __context__.
496+
is_recursive_call = _seen is not None
471497
if _seen is None:
472498
_seen = set()
473499
_seen.add(id(exc_value))
474-
# Gracefully handle (the way Python 2.4 and earlier did) the case of
475-
# being called with no type or value (None, None, None).
476-
if (exc_value and exc_value.__cause__ is not None
477-
and id(exc_value.__cause__) not in _seen):
478-
cause = TracebackException(
479-
type(exc_value.__cause__),
480-
exc_value.__cause__,
481-
exc_value.__cause__.__traceback__,
482-
limit=limit,
483-
lookup_lines=False,
484-
capture_locals=capture_locals,
485-
_seen=_seen)
486-
else:
487-
cause = None
488-
if (exc_value and exc_value.__context__ is not None
489-
and id(exc_value.__context__) not in _seen):
490-
context = TracebackException(
491-
type(exc_value.__context__),
492-
exc_value.__context__,
493-
exc_value.__context__.__traceback__,
494-
limit=limit,
495-
lookup_lines=False,
496-
capture_locals=capture_locals,
497-
_seen=_seen)
498-
else:
499-
context = None
500-
self.exc_traceback = exc_traceback
501-
self.__cause__ = cause
502-
self.__context__ = context
503-
self.__suppress_context__ = \
504-
exc_value.__suppress_context__ if exc_value else False
500+
505501
# TODO: locals.
506502
self.stack = StackSummary.extract(
507503
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
@@ -513,12 +509,62 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
513509
if exc_type and issubclass(exc_type, SyntaxError):
514510
# Handle SyntaxError's specially
515511
self.filename = exc_value.filename
516-
self.lineno = str(exc_value.lineno)
512+
lno = exc_value.lineno
513+
self.lineno = str(lno) if lno is not None else None
514+
end_lno = exc_value.end_lineno
515+
self.end_lineno = str(end_lno) if end_lno is not None else None
517516
self.text = exc_value.text
518517
self.offset = exc_value.offset
518+
self.end_offset = exc_value.end_offset
519519
self.msg = exc_value.msg
520520
if lookup_lines:
521521
self._load_lines()
522+
self.__suppress_context__ = \
523+
exc_value.__suppress_context__ if exc_value is not None else False
524+
525+
# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
526+
# queue to avoid recursion (only the top-level call gets _seen == None)
527+
if not is_recursive_call:
528+
queue = [(self, exc_value)]
529+
while queue:
530+
te, e = queue.pop()
531+
if (e and e.__cause__ is not None
532+
and id(e.__cause__) not in _seen):
533+
cause = TracebackException(
534+
type(e.__cause__),
535+
e.__cause__,
536+
e.__cause__.__traceback__,
537+
limit=limit,
538+
lookup_lines=lookup_lines,
539+
capture_locals=capture_locals,
540+
_seen=_seen)
541+
else:
542+
cause = None
543+
544+
if compact:
545+
need_context = (cause is None and
546+
e is not None and
547+
not e.__suppress_context__)
548+
else:
549+
need_context = True
550+
if (e and e.__context__ is not None
551+
and need_context and id(e.__context__) not in _seen):
552+
context = TracebackException(
553+
type(e.__context__),
554+
e.__context__,
555+
e.__context__.__traceback__,
556+
limit=limit,
557+
lookup_lines=lookup_lines,
558+
capture_locals=capture_locals,
559+
_seen=_seen)
560+
else:
561+
context = None
562+
te.__cause__ = cause
563+
te.__context__ = context
564+
if cause:
565+
queue.append((te.__cause__, e.__cause__))
566+
if context:
567+
queue.append((te.__context__, e.__context__))
522568

523569
@classmethod
524570
def from_exception(cls, exc, *args, **kwargs):
@@ -529,13 +575,11 @@ def _load_lines(self):
529575
"""Private API. force all lines in the stack to be loaded."""
530576
for frame in self.stack:
531577
frame.line
532-
if self.__context__:
533-
self.__context__._load_lines()
534-
if self.__cause__:
535-
self.__cause__._load_lines()
536578

537579
def __eq__(self, other):
538-
return self.__dict__ == other.__dict__
580+
if isinstance(other, TracebackException):
581+
return self.__dict__ == other.__dict__
582+
return NotImplemented
539583

540584
def __str__(self):
541585
return self._str
@@ -546,7 +590,7 @@ def format_exception_only(self):
546590
The return value is a generator of strings, each ending in a newline.
547591
548592
Normally, the generator emits a single string; however, for
549-
SyntaxError exceptions, it emites several lines that (when
593+
SyntaxError exceptions, it emits several lines that (when
550594
printed) display detailed information about where the syntax
551595
error occurred.
552596
@@ -560,30 +604,50 @@ def format_exception_only(self):
560604
stype = self.exc_type.__qualname__
561605
smod = self.exc_type.__module__
562606
if smod not in ("__main__", "builtins"):
607+
if not isinstance(smod, str):
608+
smod = "<unknown>"
563609
stype = smod + '.' + stype
564610

565611
if not issubclass(self.exc_type, SyntaxError):
566612
yield _format_final_exc_line(stype, self._str)
567-
return
568-
569-
# It was a syntax error; show exactly where the problem was found.
570-
filename = self.filename or "<string>"
571-
lineno = str(self.lineno) or '?'
572-
yield ' File "{}", line {}\n'.format(filename, lineno)
573-
574-
badline = self.text
575-
offset = self.offset
576-
if badline is not None:
577-
yield ' {}\n'.format(badline.strip())
578-
if offset is not None:
579-
caretspace = badline.rstrip('\n')
580-
offset = min(len(caretspace), offset) - 1
581-
caretspace = caretspace[:offset].lstrip()
582-
# non-space whitespace (likes tabs) must be kept for alignment
583-
caretspace = ((c.isspace() and c or ' ') for c in caretspace)
584-
yield ' {}^\n'.format(''.join(caretspace))
613+
else:
614+
yield from self._format_syntax_error(stype)
615+
616+
def _format_syntax_error(self, stype):
617+
"""Format SyntaxError exceptions (internal helper)."""
618+
# Show exactly where the problem was found.
619+
filename_suffix = ''
620+
if self.lineno is not None:
621+
yield ' File "{}", line {}\n'.format(
622+
self.filename or "<string>", self.lineno)
623+
elif self.filename is not None:
624+
filename_suffix = ' ({})'.format(self.filename)
625+
626+
text = self.text
627+
if text is not None:
628+
# text = " foo\n"
629+
# rtext = " foo"
630+
# ltext = "foo"
631+
rtext = text.rstrip('\n')
632+
ltext = rtext.lstrip(' \n\f')
633+
spaces = len(rtext) - len(ltext)
634+
yield ' {}\n'.format(ltext)
635+
636+
if self.offset is not None:
637+
offset = self.offset
638+
end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
639+
if offset == end_offset or end_offset == -1:
640+
end_offset = offset + 1
641+
642+
# Convert 1-based column offset to 0-based index into stripped text
643+
colno = offset - 1 - spaces
644+
end_colno = end_offset - 1 - spaces
645+
if colno >= 0:
646+
# non-space whitespace (likes tabs) must be kept for alignment
647+
caretspace = ((c if c.isspace() else ' ') for c in ltext[:colno])
648+
yield ' {}{}'.format("".join(caretspace), ('^' * (end_colno - colno) + "\n"))
585649
msg = self.msg or "<no detail available>"
586-
yield "{}: {}\n".format(stype, msg)
650+
yield "{}: {}{}\n".format(stype, msg, filename_suffix)
587651

588652
def format(self, *, chain=True):
589653
"""Format the exception.
@@ -597,15 +661,32 @@ def format(self, *, chain=True):
597661
The message indicating which exception occurred is always the last
598662
string in the output.
599663
"""
600-
if chain:
601-
if self.__cause__ is not None:
602-
yield from self.__cause__.format(chain=chain)
603-
yield _cause_message
604-
elif (self.__context__ is not None and
605-
not self.__suppress_context__):
606-
yield from self.__context__.format(chain=chain)
607-
yield _context_message
608-
if self.exc_traceback is not None:
609-
yield 'Traceback (most recent call last):\n'
610-
yield from self.stack.format()
611-
yield from self.format_exception_only()
664+
665+
output = []
666+
exc = self
667+
while exc:
668+
if chain:
669+
if exc.__cause__ is not None:
670+
chained_msg = _cause_message
671+
chained_exc = exc.__cause__
672+
elif (exc.__context__ is not None and
673+
not exc.__suppress_context__):
674+
chained_msg = _context_message
675+
chained_exc = exc.__context__
676+
else:
677+
chained_msg = None
678+
chained_exc = None
679+
680+
output.append((chained_msg, exc))
681+
exc = chained_exc
682+
else:
683+
output.append((None, exc))
684+
exc = None
685+
686+
for msg, exc in reversed(output):
687+
if msg is not None:
688+
yield msg
689+
if exc.stack:
690+
yield 'Traceback (most recent call last):\n'
691+
yield from exc.stack.format()
692+
yield from exc.format_exception_only()

0 commit comments

Comments
 (0)