##// 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 27 *.swp
28 28 .pytest_cache
29 29 .python-version
30 .venv*/
30 31 venv*/
31 32 .mypy_cache/
32 33
@@ -51,24 +51,24 b' def recursionlimit(frames):'
51 51 class ChangedPyFileTest(unittest.TestCase):
52 52 def test_changing_py_file(self):
53 53 """Traceback produced if the line where the error occurred is missing?
54
54
55 55 https://github.com/ipython/ipython/issues/1456
56 56 """
57 57 with TemporaryDirectory() as td:
58 58 fname = os.path.join(td, "foo.py")
59 59 with open(fname, "w", encoding="utf-8") as f:
60 60 f.write(file_1)
61
61
62 62 with prepended_to_syspath(td):
63 63 ip.run_cell("import foo")
64
64
65 65 with tt.AssertPrints("ZeroDivisionError"):
66 66 ip.run_cell("foo.f()")
67
67
68 68 # Make the file shorter, so the line of the error is missing.
69 69 with open(fname, "w", encoding="utf-8") as f:
70 70 f.write(file_2)
71
71
72 72 # For some reason, this was failing on the *second* call after
73 73 # changing the file, so we call f() twice.
74 74 with tt.AssertNotPrints("Internal Python error", channel='stderr'):
@@ -92,27 +92,27 b' class NonAsciiTest(unittest.TestCase):'
92 92 fname = os.path.join(td, u"fooΓ©.py")
93 93 with open(fname, "w", encoding="utf-8") as f:
94 94 f.write(file_1)
95
95
96 96 with prepended_to_syspath(td):
97 97 ip.run_cell("import foo")
98
98
99 99 with tt.AssertPrints("ZeroDivisionError"):
100 100 ip.run_cell("foo.f()")
101
101
102 102 def test_iso8859_5(self):
103 103 with TemporaryDirectory() as td:
104 104 fname = os.path.join(td, 'dfghjkl.py')
105 105
106 106 with io.open(fname, 'w', encoding='iso-8859-5') as f:
107 107 f.write(iso_8859_5_file)
108
108
109 109 with prepended_to_syspath(td):
110 110 ip.run_cell("from dfghjkl import fail")
111
111
112 112 with tt.AssertPrints("ZeroDivisionError"):
113 113 with tt.AssertPrints(u'Π΄Π±Π˜Π–', suppress=False):
114 114 ip.run_cell('fail()')
115
115
116 116 def test_nonascii_msg(self):
117 117 cell = u"raise Exception('Γ©')"
118 118 expected = u"Exception('Γ©')"
@@ -167,12 +167,12 b' class IndentationErrorTest(unittest.TestCase):'
167 167 with tt.AssertPrints("IndentationError"):
168 168 with tt.AssertPrints("zoon()", suppress=False):
169 169 ip.run_cell(indentationerror_file)
170
170
171 171 with TemporaryDirectory() as td:
172 172 fname = os.path.join(td, "foo.py")
173 173 with open(fname, "w", encoding="utf-8") as f:
174 174 f.write(indentationerror_file)
175
175
176 176 with tt.AssertPrints("IndentationError"):
177 177 with tt.AssertPrints("zoon()", suppress=False):
178 178 ip.magic('run %s' % fname)
@@ -363,6 +363,29 b' def r3o2():'
363 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 391 # module testing (minimal)
@@ -89,6 +89,7 b' Inheritance diagram:'
89 89 #*****************************************************************************
90 90
91 91
92 from collections.abc import Sequence
92 93 import functools
93 94 import inspect
94 95 import linecache
@@ -183,6 +184,14 b' def get_line_number_of_frame(frame: types.FrameType) -> int:'
183 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 195 def _format_traceback_lines(lines, Colors, has_colors: bool, lvals):
187 196 """
188 197 Format tracebacks lines with pointing arrow, leading numbers...
@@ -582,7 +591,7 b' class ListTB(TBTools):'
582 591 """
583 592
584 593 Colors = self.Colors
585 list = []
594 output_list = []
586 595 for ind, (filename, lineno, name, line) in enumerate(extracted_list):
587 596 normalCol, nameCol, fileCol, lineCol = (
588 597 # Emphasize the last entry
@@ -600,9 +609,9 b' class ListTB(TBTools):'
600 609 item += "\n"
601 610 if line:
602 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 616 def _format_exception_only(self, etype, value):
608 617 """Format the exception part of a traceback.
@@ -619,11 +628,11 b' class ListTB(TBTools):'
619 628 """
620 629 have_filedata = False
621 630 Colors = self.Colors
622 list = []
631 output_list = []
623 632 stype = py3compat.cast_unicode(Colors.excName + etype.__name__ + Colors.Normal)
624 633 if value is None:
625 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 636 else:
628 637 if issubclass(etype, SyntaxError):
629 638 have_filedata = True
@@ -634,7 +643,7 b' class ListTB(TBTools):'
634 643 else:
635 644 lineno = "unknown"
636 645 textline = ""
637 list.append(
646 output_list.append(
638 647 "%s %s%s\n"
639 648 % (
640 649 Colors.normalEm,
@@ -654,28 +663,33 b' class ListTB(TBTools):'
654 663 i = 0
655 664 while i < len(textline) and textline[i].isspace():
656 665 i += 1
657 list.append('%s %s%s\n' % (Colors.line,
658 textline.strip(),
659 Colors.Normal))
666 output_list.append(
667 "%s %s%s\n" % (Colors.line, textline.strip(), Colors.Normal)
668 )
660 669 if value.offset is not None:
661 670 s = ' '
662 671 for c in textline[i:value.offset - 1]:
663 672 if c.isspace():
664 673 s += c
665 674 else:
666 s += ' '
667 list.append('%s%s^%s\n' % (Colors.caret, s,
668 Colors.Normal))
675 s += " "
676 output_list.append(
677 "%s%s^%s\n" % (Colors.caret, s, Colors.Normal)
678 )
669 679
670 680 try:
671 681 s = value.msg
672 682 except Exception:
673 683 s = self._some_str(value)
674 684 if s:
675 list.append('%s%s:%s %s\n' % (stype, Colors.excName,
676 Colors.Normal, s))
685 output_list.append(
686 "%s%s:%s %s\n" % (stype, Colors.excName, Colors.Normal, s)
687 )
677 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 694 # sync with user hooks
681 695 if have_filedata:
@@ -683,7 +697,7 b' class ListTB(TBTools):'
683 697 if ipinst is not None:
684 698 ipinst.hooks.synchronize_with_editor(value.filename, value.lineno, 0)
685 699
686 return list
700 return output_list
687 701
688 702 def get_exception_only(self, etype, value):
689 703 """Only print the exception type and message, without a traceback.
@@ -999,9 +1013,27 b' class VerboseTB(TBTools):'
999 1013 # User exception is improperly defined.
1000 1014 etype, evalue = str, sys.exc_info()[:2]
1001 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 1022 # ... and format it
1003 return ['%s%s%s: %s' % (colors.excName, etype_str,
1004 colorsnormal, py3compat.cast_unicode(evalue_str))]
1023 return [
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 1038 def format_exception_as_a_whole(
1007 1039 self,
@@ -1068,7 +1100,7 b' class VerboseTB(TBTools):'
1068 1100 if ipinst is not None:
1069 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 1105 def get_records(
1074 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