##// END OF EJS Templates
Display exception notes in tracebacks (#14039)...
Matthias Bussonnier -
r28313:1d4e1847 merge
parent child Browse files
Show More
@@ -0,0 +1,5 b''
1 Support for PEP-678 Exception Notes
2 -----------------------------------
3
4 Ultratb now shows :pep:`678` notes, improving your debugging experience on
5 Python 3.11+ or with libraries such as Pytest and Hypothesis.
@@ -27,6 +27,7 b' __pycache__'
27 *.swp
27 *.swp
28 .pytest_cache
28 .pytest_cache
29 .python-version
29 .python-version
30 .venv*/
30 venv*/
31 venv*/
31 .mypy_cache/
32 .mypy_cache/
32
33
@@ -51,24 +51,24 b' def recursionlimit(frames):'
51 class ChangedPyFileTest(unittest.TestCase):
51 class ChangedPyFileTest(unittest.TestCase):
52 def test_changing_py_file(self):
52 def test_changing_py_file(self):
53 """Traceback produced if the line where the error occurred is missing?
53 """Traceback produced if the line where the error occurred is missing?
54
54
55 https://github.com/ipython/ipython/issues/1456
55 https://github.com/ipython/ipython/issues/1456
56 """
56 """
57 with TemporaryDirectory() as td:
57 with TemporaryDirectory() as td:
58 fname = os.path.join(td, "foo.py")
58 fname = os.path.join(td, "foo.py")
59 with open(fname, "w", encoding="utf-8") as f:
59 with open(fname, "w", encoding="utf-8") as f:
60 f.write(file_1)
60 f.write(file_1)
61
61
62 with prepended_to_syspath(td):
62 with prepended_to_syspath(td):
63 ip.run_cell("import foo")
63 ip.run_cell("import foo")
64
64
65 with tt.AssertPrints("ZeroDivisionError"):
65 with tt.AssertPrints("ZeroDivisionError"):
66 ip.run_cell("foo.f()")
66 ip.run_cell("foo.f()")
67
67
68 # Make the file shorter, so the line of the error is missing.
68 # Make the file shorter, so the line of the error is missing.
69 with open(fname, "w", encoding="utf-8") as f:
69 with open(fname, "w", encoding="utf-8") as f:
70 f.write(file_2)
70 f.write(file_2)
71
71
72 # For some reason, this was failing on the *second* call after
72 # For some reason, this was failing on the *second* call after
73 # changing the file, so we call f() twice.
73 # changing the file, so we call f() twice.
74 with tt.AssertNotPrints("Internal Python error", channel='stderr'):
74 with tt.AssertNotPrints("Internal Python error", channel='stderr'):
@@ -92,27 +92,27 b' class NonAsciiTest(unittest.TestCase):'
92 fname = os.path.join(td, u"fooΓ©.py")
92 fname = os.path.join(td, u"fooΓ©.py")
93 with open(fname, "w", encoding="utf-8") as f:
93 with open(fname, "w", encoding="utf-8") as f:
94 f.write(file_1)
94 f.write(file_1)
95
95
96 with prepended_to_syspath(td):
96 with prepended_to_syspath(td):
97 ip.run_cell("import foo")
97 ip.run_cell("import foo")
98
98
99 with tt.AssertPrints("ZeroDivisionError"):
99 with tt.AssertPrints("ZeroDivisionError"):
100 ip.run_cell("foo.f()")
100 ip.run_cell("foo.f()")
101
101
102 def test_iso8859_5(self):
102 def test_iso8859_5(self):
103 with TemporaryDirectory() as td:
103 with TemporaryDirectory() as td:
104 fname = os.path.join(td, 'dfghjkl.py')
104 fname = os.path.join(td, 'dfghjkl.py')
105
105
106 with io.open(fname, 'w', encoding='iso-8859-5') as f:
106 with io.open(fname, 'w', encoding='iso-8859-5') as f:
107 f.write(iso_8859_5_file)
107 f.write(iso_8859_5_file)
108
108
109 with prepended_to_syspath(td):
109 with prepended_to_syspath(td):
110 ip.run_cell("from dfghjkl import fail")
110 ip.run_cell("from dfghjkl import fail")
111
111
112 with tt.AssertPrints("ZeroDivisionError"):
112 with tt.AssertPrints("ZeroDivisionError"):
113 with tt.AssertPrints(u'Π΄Π±Π˜Π–', suppress=False):
113 with tt.AssertPrints(u'Π΄Π±Π˜Π–', suppress=False):
114 ip.run_cell('fail()')
114 ip.run_cell('fail()')
115
115
116 def test_nonascii_msg(self):
116 def test_nonascii_msg(self):
117 cell = u"raise Exception('Γ©')"
117 cell = u"raise Exception('Γ©')"
118 expected = u"Exception('Γ©')"
118 expected = u"Exception('Γ©')"
@@ -167,12 +167,12 b' class IndentationErrorTest(unittest.TestCase):'
167 with tt.AssertPrints("IndentationError"):
167 with tt.AssertPrints("IndentationError"):
168 with tt.AssertPrints("zoon()", suppress=False):
168 with tt.AssertPrints("zoon()", suppress=False):
169 ip.run_cell(indentationerror_file)
169 ip.run_cell(indentationerror_file)
170
170
171 with TemporaryDirectory() as td:
171 with TemporaryDirectory() as td:
172 fname = os.path.join(td, "foo.py")
172 fname = os.path.join(td, "foo.py")
173 with open(fname, "w", encoding="utf-8") as f:
173 with open(fname, "w", encoding="utf-8") as f:
174 f.write(indentationerror_file)
174 f.write(indentationerror_file)
175
175
176 with tt.AssertPrints("IndentationError"):
176 with tt.AssertPrints("IndentationError"):
177 with tt.AssertPrints("zoon()", suppress=False):
177 with tt.AssertPrints("zoon()", suppress=False):
178 ip.magic('run %s' % fname)
178 ip.magic('run %s' % fname)
@@ -363,6 +363,29 b' def r3o2():'
363 ip.run_cell("r3o2()")
363 ip.run_cell("r3o2()")
364
364
365
365
366 class PEP678NotesReportingTest(unittest.TestCase):
367 ERROR_WITH_NOTE = """
368 try:
369 raise AssertionError("Message")
370 except Exception as e:
371 try:
372 e.add_note("This is a PEP-678 note.")
373 except AttributeError: # Python <= 3.10
374 e.__notes__ = ("This is a PEP-678 note.",)
375 raise
376 """
377
378 def test_verbose_reports_notes(self):
379 with tt.AssertPrints(["AssertionError", "Message", "This is a PEP-678 note."]):
380 ip.run_cell(self.ERROR_WITH_NOTE)
381
382 def test_plain_reports_notes(self):
383 with tt.AssertPrints(["AssertionError", "Message", "This is a PEP-678 note."]):
384 ip.run_cell("%xmode Plain")
385 ip.run_cell(self.ERROR_WITH_NOTE)
386 ip.run_cell("%xmode Verbose")
387
388
366 #----------------------------------------------------------------------------
389 #----------------------------------------------------------------------------
367
390
368 # module testing (minimal)
391 # module testing (minimal)
@@ -89,6 +89,7 b' Inheritance diagram:'
89 #*****************************************************************************
89 #*****************************************************************************
90
90
91
91
92 from collections.abc import Sequence
92 import functools
93 import functools
93 import inspect
94 import inspect
94 import linecache
95 import linecache
@@ -183,6 +184,14 b' def get_line_number_of_frame(frame: types.FrameType) -> int:'
183 return count_lines_in_py_file(filename)
184 return count_lines_in_py_file(filename)
184
185
185
186
187 def _safe_string(value, what, func=str):
188 # Copied from cpython/Lib/traceback.py
189 try:
190 return func(value)
191 except:
192 return f"<{what} {func.__name__}() failed>"
193
194
186 def _format_traceback_lines(lines, Colors, has_colors: bool, lvals):
195 def _format_traceback_lines(lines, Colors, has_colors: bool, lvals):
187 """
196 """
188 Format tracebacks lines with pointing arrow, leading numbers...
197 Format tracebacks lines with pointing arrow, leading numbers...
@@ -582,7 +591,7 b' class ListTB(TBTools):'
582 """
591 """
583
592
584 Colors = self.Colors
593 Colors = self.Colors
585 list = []
594 output_list = []
586 for ind, (filename, lineno, name, line) in enumerate(extracted_list):
595 for ind, (filename, lineno, name, line) in enumerate(extracted_list):
587 normalCol, nameCol, fileCol, lineCol = (
596 normalCol, nameCol, fileCol, lineCol = (
588 # Emphasize the last entry
597 # Emphasize the last entry
@@ -600,9 +609,9 b' class ListTB(TBTools):'
600 item += "\n"
609 item += "\n"
601 if line:
610 if line:
602 item += f"{lineCol} {line.strip()}{normalCol}\n"
611 item += f"{lineCol} {line.strip()}{normalCol}\n"
603 list.append(item)
612 output_list.append(item)
604
613
605 return list
614 return output_list
606
615
607 def _format_exception_only(self, etype, value):
616 def _format_exception_only(self, etype, value):
608 """Format the exception part of a traceback.
617 """Format the exception part of a traceback.
@@ -619,11 +628,11 b' class ListTB(TBTools):'
619 """
628 """
620 have_filedata = False
629 have_filedata = False
621 Colors = self.Colors
630 Colors = self.Colors
622 list = []
631 output_list = []
623 stype = py3compat.cast_unicode(Colors.excName + etype.__name__ + Colors.Normal)
632 stype = py3compat.cast_unicode(Colors.excName + etype.__name__ + Colors.Normal)
624 if value is None:
633 if value is None:
625 # Not sure if this can still happen in Python 2.6 and above
634 # Not sure if this can still happen in Python 2.6 and above
626 list.append(stype + '\n')
635 output_list.append(stype + "\n")
627 else:
636 else:
628 if issubclass(etype, SyntaxError):
637 if issubclass(etype, SyntaxError):
629 have_filedata = True
638 have_filedata = True
@@ -634,7 +643,7 b' class ListTB(TBTools):'
634 else:
643 else:
635 lineno = "unknown"
644 lineno = "unknown"
636 textline = ""
645 textline = ""
637 list.append(
646 output_list.append(
638 "%s %s%s\n"
647 "%s %s%s\n"
639 % (
648 % (
640 Colors.normalEm,
649 Colors.normalEm,
@@ -654,28 +663,33 b' class ListTB(TBTools):'
654 i = 0
663 i = 0
655 while i < len(textline) and textline[i].isspace():
664 while i < len(textline) and textline[i].isspace():
656 i += 1
665 i += 1
657 list.append('%s %s%s\n' % (Colors.line,
666 output_list.append(
658 textline.strip(),
667 "%s %s%s\n" % (Colors.line, textline.strip(), Colors.Normal)
659 Colors.Normal))
668 )
660 if value.offset is not None:
669 if value.offset is not None:
661 s = ' '
670 s = ' '
662 for c in textline[i:value.offset - 1]:
671 for c in textline[i:value.offset - 1]:
663 if c.isspace():
672 if c.isspace():
664 s += c
673 s += c
665 else:
674 else:
666 s += ' '
675 s += " "
667 list.append('%s%s^%s\n' % (Colors.caret, s,
676 output_list.append(
668 Colors.Normal))
677 "%s%s^%s\n" % (Colors.caret, s, Colors.Normal)
678 )
669
679
670 try:
680 try:
671 s = value.msg
681 s = value.msg
672 except Exception:
682 except Exception:
673 s = self._some_str(value)
683 s = self._some_str(value)
674 if s:
684 if s:
675 list.append('%s%s:%s %s\n' % (stype, Colors.excName,
685 output_list.append(
676 Colors.Normal, s))
686 "%s%s:%s %s\n" % (stype, Colors.excName, Colors.Normal, s)
687 )
677 else:
688 else:
678 list.append('%s\n' % stype)
689 output_list.append("%s\n" % stype)
690
691 # PEP-678 notes
692 output_list.extend(f"{x}\n" for x in getattr(value, "__notes__", []))
679
693
680 # sync with user hooks
694 # sync with user hooks
681 if have_filedata:
695 if have_filedata:
@@ -683,7 +697,7 b' class ListTB(TBTools):'
683 if ipinst is not None:
697 if ipinst is not None:
684 ipinst.hooks.synchronize_with_editor(value.filename, value.lineno, 0)
698 ipinst.hooks.synchronize_with_editor(value.filename, value.lineno, 0)
685
699
686 return list
700 return output_list
687
701
688 def get_exception_only(self, etype, value):
702 def get_exception_only(self, etype, value):
689 """Only print the exception type and message, without a traceback.
703 """Only print the exception type and message, without a traceback.
@@ -999,9 +1013,27 b' class VerboseTB(TBTools):'
999 # User exception is improperly defined.
1013 # User exception is improperly defined.
1000 etype, evalue = str, sys.exc_info()[:2]
1014 etype, evalue = str, sys.exc_info()[:2]
1001 etype_str, evalue_str = map(str, (etype, evalue))
1015 etype_str, evalue_str = map(str, (etype, evalue))
1016
1017 # PEP-678 notes
1018 notes = getattr(evalue, "__notes__", [])
1019 if not isinstance(notes, Sequence) or isinstance(notes, (str, bytes)):
1020 notes = [_safe_string(notes, "__notes__", func=repr)]
1021
1002 # ... and format it
1022 # ... and format it
1003 return ['%s%s%s: %s' % (colors.excName, etype_str,
1023 return [
1004 colorsnormal, py3compat.cast_unicode(evalue_str))]
1024 "{}{}{}: {}".format(
1025 colors.excName,
1026 etype_str,
1027 colorsnormal,
1028 py3compat.cast_unicode(evalue_str),
1029 ),
1030 *(
1031 "{}{}".format(
1032 colorsnormal, _safe_string(py3compat.cast_unicode(n), "note")
1033 )
1034 for n in notes
1035 ),
1036 ]
1005
1037
1006 def format_exception_as_a_whole(
1038 def format_exception_as_a_whole(
1007 self,
1039 self,
@@ -1068,7 +1100,7 b' class VerboseTB(TBTools):'
1068 if ipinst is not None:
1100 if ipinst is not None:
1069 ipinst.hooks.synchronize_with_editor(frame_info.filename, frame_info.lineno, 0)
1101 ipinst.hooks.synchronize_with_editor(frame_info.filename, frame_info.lineno, 0)
1070
1102
1071 return [[head] + frames + [''.join(formatted_exception[0])]]
1103 return [[head] + frames + formatted_exception]
1072
1104
1073 def get_records(
1105 def get_records(
1074 self, etb: TracebackType, number_of_lines_of_context: int, tb_offset: int
1106 self, etb: TracebackType, number_of_lines_of_context: int, tb_offset: int
General Comments 0
You need to be logged in to leave comments. Login now