@@ -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
289309def 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