summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorIrit Katriel <1055913+iritkatriel@users.noreply.github.com>2022-04-16 18:59:52 (GMT)
committerGitHub <noreply@github.com>2022-04-16 18:59:52 (GMT)
commitd4c4a76ed1427c947fcbbe692625b3f644cf3aaf (patch)
tree2e503da40ff6459711ff5730b22e89962b175252 /Lib
parent7fa3a5a2197896066e3fe53ee325ac6ab54c3414 (diff)
downloadcpython-d4c4a76ed1427c947fcbbe692625b3f644cf3aaf.zip
cpython-d4c4a76ed1427c947fcbbe692625b3f644cf3aaf.tar.gz
cpython-d4c4a76ed1427c947fcbbe692625b3f644cf3aaf.tar.bz2
gh-89770: Implement PEP-678 - Exception notes (GH-31317)
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_exception_group.py35
-rw-r--r--Lib/test/test_exceptions.py32
-rw-r--r--Lib/test/test_traceback.py193
-rw-r--r--Lib/traceback.py24
4 files changed, 221 insertions, 63 deletions
diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py
index 793e8d2..2cfd873 100644
--- a/Lib/test/test_exception_group.py
+++ b/Lib/test/test_exception_group.py
@@ -567,7 +567,9 @@ class ExceptionGroupSplitTestBase(ExceptionGroupTestBase):
self.assertIs(eg.__cause__, part.__cause__)
self.assertIs(eg.__context__, part.__context__)
self.assertIs(eg.__traceback__, part.__traceback__)
- self.assertIs(eg.__note__, part.__note__)
+ self.assertEqual(
+ getattr(eg, '__notes__', None),
+ getattr(part, '__notes__', None))
def tbs_for_leaf(leaf, eg):
for e, tbs in leaf_generator(eg):
@@ -632,7 +634,7 @@ class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase):
try:
nested_group()
except ExceptionGroup as e:
- e.__note__ = f"the note: {id(e)}"
+ e.add_note(f"the note: {id(e)}")
eg = e
eg_template = [
@@ -728,6 +730,35 @@ class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase):
self.assertMatchesTemplate(
rest, ExceptionGroup, [ValueError(1)])
+ def test_split_copies_notes(self):
+ # make sure each exception group after a split has its own __notes__ list
+ eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)])
+ eg.add_note("note1")
+ eg.add_note("note2")
+ orig_notes = list(eg.__notes__)
+ match, rest = eg.split(TypeError)
+ self.assertEqual(eg.__notes__, orig_notes)
+ self.assertEqual(match.__notes__, orig_notes)
+ self.assertEqual(rest.__notes__, orig_notes)
+ self.assertIsNot(eg.__notes__, match.__notes__)
+ self.assertIsNot(eg.__notes__, rest.__notes__)
+ self.assertIsNot(match.__notes__, rest.__notes__)
+ eg.add_note("eg")
+ match.add_note("match")
+ rest.add_note("rest")
+ self.assertEqual(eg.__notes__, orig_notes + ["eg"])
+ self.assertEqual(match.__notes__, orig_notes + ["match"])
+ self.assertEqual(rest.__notes__, orig_notes + ["rest"])
+
+ def test_split_does_not_copy_non_sequence_notes(self):
+ # __notes__ should be a sequence, which is shallow copied.
+ # If it is not a sequence, the split parts don't get any notes.
+ eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)])
+ eg.__notes__ = 123
+ match, rest = eg.split(TypeError)
+ self.assertFalse(hasattr(match, '__notes__'))
+ self.assertFalse(hasattr(rest, '__notes__'))
+
class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase):
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 6dca79e..2b5b519 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -547,26 +547,32 @@ class ExceptionTests(unittest.TestCase):
'pickled "%r", attribute "%s' %
(e, checkArgName))
- def test_note(self):
+ def test_notes(self):
for e in [BaseException(1), Exception(2), ValueError(3)]:
with self.subTest(e=e):
- self.assertIsNone(e.__note__)
- e.__note__ = "My Note"
- self.assertEqual(e.__note__, "My Note")
+ self.assertFalse(hasattr(e, '__notes__'))
+ e.add_note("My Note")
+ self.assertEqual(e.__notes__, ["My Note"])
with self.assertRaises(TypeError):
- e.__note__ = 42
- self.assertEqual(e.__note__, "My Note")
+ e.add_note(42)
+ self.assertEqual(e.__notes__, ["My Note"])
- e.__note__ = "Your Note"
- self.assertEqual(e.__note__, "Your Note")
+ e.add_note("Your Note")
+ self.assertEqual(e.__notes__, ["My Note", "Your Note"])
- with self.assertRaises(TypeError):
- del e.__note__
- self.assertEqual(e.__note__, "Your Note")
+ del e.__notes__
+ self.assertFalse(hasattr(e, '__notes__'))
+
+ e.add_note("Our Note")
+ self.assertEqual(e.__notes__, ["Our Note"])
- e.__note__ = None
- self.assertIsNone(e.__note__)
+ e.__notes__ = 42
+ self.assertEqual(e.__notes__, 42)
+
+ with self.assertRaises(TypeError):
+ e.add_note("will not work")
+ self.assertEqual(e.__notes__, 42)
def testWithTraceback(self):
try:
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 75d668d..962322c 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -1323,21 +1323,80 @@ class BaseExceptionReportingTests:
self.assertEqual(exp, err)
def test_exception_with_note(self):
- e = ValueError(42)
+ e = ValueError(123)
vanilla = self.get_report(e)
- e.__note__ = 'My Note'
+ e.add_note('My Note')
self.assertEqual(self.get_report(e), vanilla + 'My Note\n')
- e.__note__ = ''
+ del e.__notes__
+ e.add_note('')
self.assertEqual(self.get_report(e), vanilla + '\n')
- e.__note__ = 'Your Note'
+ del e.__notes__
+ e.add_note('Your Note')
self.assertEqual(self.get_report(e), vanilla + 'Your Note\n')
- e.__note__ = None
+ del e.__notes__
self.assertEqual(self.get_report(e), vanilla)
+ def test_exception_with_invalid_notes(self):
+ e = ValueError(123)
+ vanilla = self.get_report(e)
+
+ # non-sequence __notes__
+ class BadThing:
+ def __str__(self):
+ return 'bad str'
+
+ def __repr__(self):
+ return 'bad repr'
+
+ # unprintable, non-sequence __notes__
+ class Unprintable:
+ def __repr__(self):
+ raise ValueError('bad value')
+
+ e.__notes__ = BadThing()
+ notes_repr = 'bad repr'
+ self.assertEqual(self.get_report(e), vanilla + notes_repr)
+
+ e.__notes__ = Unprintable()
+ err_msg = '<__notes__ repr() failed>'
+ self.assertEqual(self.get_report(e), vanilla + err_msg)
+
+ # non-string item in the __notes__ sequence
+ e.__notes__ = [BadThing(), 'Final Note']
+ bad_note = 'bad str'
+ self.assertEqual(self.get_report(e), vanilla + bad_note + '\nFinal Note\n')
+
+ # unprintable, non-string item in the __notes__ sequence
+ e.__notes__ = [Unprintable(), 'Final Note']
+ err_msg = '<note str() failed>'
+ self.assertEqual(self.get_report(e), vanilla + err_msg + '\nFinal Note\n')
+
+ def test_exception_with_note_with_multiple_notes(self):
+ e = ValueError(42)
+ vanilla = self.get_report(e)
+
+ e.add_note('Note 1')
+ e.add_note('Note 2')
+ e.add_note('Note 3')
+
+ self.assertEqual(
+ self.get_report(e),
+ vanilla + 'Note 1\n' + 'Note 2\n' + 'Note 3\n')
+
+ del e.__notes__
+ e.add_note('Note 4')
+ del e.__notes__
+ e.add_note('Note 5')
+ e.add_note('Note 6')
+
+ self.assertEqual(
+ self.get_report(e),
+ vanilla + 'Note 5\n' + 'Note 6\n')
+
def test_exception_qualname(self):
class A:
class B:
@@ -1688,16 +1747,16 @@ class BaseExceptionReportingTests:
try:
raise ValueError(msg)
except ValueError as e:
- e.__note__ = f'the {msg}'
+ e.add_note(f'the {msg}')
excs.append(e)
raise ExceptionGroup("nested", excs)
except ExceptionGroup as e:
- e.__note__ = ('>> Multi line note\n'
- '>> Because I am such\n'
- '>> an important exception.\n'
- '>> empty lines work too\n'
- '\n'
- '(that was an empty line)')
+ e.add_note(('>> Multi line note\n'
+ '>> Because I am such\n'
+ '>> an important exception.\n'
+ '>> empty lines work too\n'
+ '\n'
+ '(that was an empty line)'))
raise
expected = (f' + Exception Group Traceback (most recent call last):\n'
@@ -1733,6 +1792,64 @@ class BaseExceptionReportingTests:
report = self.get_report(exc)
self.assertEqual(report, expected)
+ def test_exception_group_with_multiple_notes(self):
+ def exc():
+ try:
+ excs = []
+ for msg in ['bad value', 'terrible value']:
+ try:
+ raise ValueError(msg)
+ except ValueError as e:
+ e.add_note(f'the {msg}')
+ e.add_note(f'Goodbye {msg}')
+ excs.append(e)
+ raise ExceptionGroup("nested", excs)
+ except ExceptionGroup as e:
+ e.add_note(('>> Multi line note\n'
+ '>> Because I am such\n'
+ '>> an important exception.\n'
+ '>> empty lines work too\n'
+ '\n'
+ '(that was an empty line)'))
+ e.add_note('Goodbye!')
+ raise
+
+ expected = (f' + Exception Group Traceback (most recent call last):\n'
+ f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
+ f' | exception_or_callable()\n'
+ f' | ^^^^^^^^^^^^^^^^^^^^^^^\n'
+ f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n'
+ f' | raise ExceptionGroup("nested", excs)\n'
+ f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
+ f' | ExceptionGroup: nested (2 sub-exceptions)\n'
+ f' | >> Multi line note\n'
+ f' | >> Because I am such\n'
+ f' | >> an important exception.\n'
+ f' | >> empty lines work too\n'
+ f' | \n'
+ f' | (that was an empty line)\n'
+ f' | Goodbye!\n'
+ f' +-+---------------- 1 ----------------\n'
+ f' | Traceback (most recent call last):\n'
+ f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n'
+ f' | raise ValueError(msg)\n'
+ f' | ^^^^^^^^^^^^^^^^^^^^^\n'
+ f' | ValueError: bad value\n'
+ f' | the bad value\n'
+ f' | Goodbye bad value\n'
+ f' +---------------- 2 ----------------\n'
+ f' | Traceback (most recent call last):\n'
+ f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n'
+ f' | raise ValueError(msg)\n'
+ f' | ^^^^^^^^^^^^^^^^^^^^^\n'
+ f' | ValueError: terrible value\n'
+ f' | the terrible value\n'
+ f' | Goodbye terrible value\n'
+ f' +------------------------------------\n')
+
+ report = self.get_report(exc)
+ self.assertEqual(report, expected)
+
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
#
@@ -2077,32 +2194,32 @@ class TestStack(unittest.TestCase):
[f'{__file__}:{some_inner.__code__.co_firstlineno + 1}'])
def test_dropping_frames(self):
- def f():
- 1/0
-
- def g():
- try:
- f()
- except:
- return sys.exc_info()
-
- exc_info = g()
-
- class Skip_G(traceback.StackSummary):
- def format_frame_summary(self, frame_summary):
- if frame_summary.name == 'g':
- return None
- return super().format_frame_summary(frame_summary)
-
- stack = Skip_G.extract(
- traceback.walk_tb(exc_info[2])).format()
-
- self.assertEqual(len(stack), 1)
- lno = f.__code__.co_firstlineno + 1
- self.assertEqual(
- stack[0],
- f' File "{__file__}", line {lno}, in f\n 1/0\n'
- )
+ def f():
+ 1/0
+
+ def g():
+ try:
+ f()
+ except:
+ return sys.exc_info()
+
+ exc_info = g()
+
+ class Skip_G(traceback.StackSummary):
+ def format_frame_summary(self, frame_summary):
+ if frame_summary.name == 'g':
+ return None
+ return super().format_frame_summary(frame_summary)
+
+ stack = Skip_G.extract(
+ traceback.walk_tb(exc_info[2])).format()
+
+ self.assertEqual(len(stack), 1)
+ lno = f.__code__.co_firstlineno + 1
+ self.assertEqual(
+ stack[0],
+ f' File "{__file__}", line {lno}, in f\n 1/0\n'
+ )
class TestTracebackException(unittest.TestCase):
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 05f1fff..3afe49d 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1,6 +1,6 @@
"""Extract, format and print information about Python stack traces."""
-import collections
+import collections.abc
import itertools
import linecache
import sys
@@ -163,18 +163,18 @@ def format_exception_only(exc, /, value=_sentinel):
# -- not official API but folk probably use these two functions.
def _format_final_exc_line(etype, value):
- valuestr = _some_str(value)
+ valuestr = _safe_string(value, 'exception')
if value is None or not valuestr:
line = "%s\n" % etype
else:
line = "%s: %s\n" % (etype, valuestr)
return line
-def _some_str(value):
+def _safe_string(value, what, func=str):
try:
- return str(value)
+ return func(value)
except:
- return '<exception str() failed>'
+ return f'<{what} {func.__name__}() failed>'
# --
@@ -688,8 +688,8 @@ class TracebackException:
self.exc_type = exc_type
# Capture now to permit freeing resources: only complication is in the
# unofficial API _format_final_exc_line
- self._str = _some_str(exc_value)
- self.__note__ = exc_value.__note__ if exc_value else None
+ self._str = _safe_string(exc_value, 'exception')
+ self.__notes__ = getattr(exc_value, '__notes__', None)
if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially
@@ -822,8 +822,12 @@ class TracebackException:
yield _format_final_exc_line(stype, self._str)
else:
yield from self._format_syntax_error(stype)
- if self.__note__ is not None:
- yield from [l + '\n' for l in self.__note__.split('\n')]
+ if isinstance(self.__notes__, collections.abc.Sequence):
+ for note in self.__notes__:
+ note = _safe_string(note, 'note')
+ yield from [l + '\n' for l in note.split('\n')]
+ elif self.__notes__ is not None:
+ yield _safe_string(self.__notes__, '__notes__', func=repr)
def _format_syntax_error(self, stype):
"""Format SyntaxError exceptions (internal helper)."""
@@ -913,7 +917,7 @@ class TracebackException:
# format exception group
is_toplevel = (_ctx.exception_group_depth == 0)
if is_toplevel:
- _ctx.exception_group_depth += 1
+ _ctx.exception_group_depth += 1
if exc.stack:
yield from _ctx.emit(