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 + |
|
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( |
|
666 | output_list.append( | |
658 |
|
|
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( |
|
676 | output_list.append( | |
668 |
|
|
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( |
|
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 + |
|
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