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 + |
|
|
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( |
|
|
658 |
|
|
|
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( |
|
|
668 |
|
|
|
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( |
|
|
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 + |
|
|
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