Skip to content

Commit 1555087

Browse files
committed
PEP-678: exception notes are set by add_note(). __notes__ holds a tuple of the assigned notes
1 parent 6331c08 commit 1555087

File tree

11 files changed

+230
-103
lines changed

11 files changed

+230
-103
lines changed

Doc/library/exceptions.rst

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,22 @@ The following exceptions are used mostly as base classes for other exceptions.
126126
tb = sys.exc_info()[2]
127127
raise OtherException(...).with_traceback(tb)
128128

129-
.. attribute:: __note__
129+
.. method:: add_note(note, / replace=False)
130130

131-
A mutable field which is :const:`None` by default and can be set to a string.
132-
If it is not :const:`None`, it is included in the traceback. This field can
133-
be used to enrich exceptions after they have been caught.
131+
If ``note`` is a string, it is added to the exception's notes which appear
132+
in the standard traceback after the exception string. If ``replace`` is
133+
true, all previously existing notes are removed before the new one is added.
134+
To clear all notes, use ``add_note(None, replace=True)``. A :exc:`TypeError`
135+
is raise if ``note`` is neither a string nor ``None``.
134136

135-
.. versionadded:: 3.11
137+
.. versionadded:: 3.11
138+
139+
.. attribute:: __notes__
140+
141+
A read-only field that contains a tuple of the notes of this exception.
142+
See :meth:`add_note`.
143+
144+
.. versionadded:: 3.11
136145

137146

138147
.. exception:: Exception
@@ -898,7 +907,7 @@ their subgroups based on the types of the contained exceptions.
898907

899908
The nesting structure of the current exception is preserved in the result,
900909
as are the values of its :attr:`message`, :attr:`__traceback__`,
901-
:attr:`__cause__`, :attr:`__context__` and :attr:`__note__` fields.
910+
:attr:`__cause__`, :attr:`__context__` and :attr:`__notes__` fields.
902911
Empty nested groups are omitted from the result.
903912

904913
The condition is checked for all exceptions in the nested exception group,
@@ -915,7 +924,7 @@ their subgroups based on the types of the contained exceptions.
915924

916925
Returns an exception group with the same :attr:`message`,
917926
:attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__`
918-
and :attr:`__note__` but which wraps the exceptions in ``excs``.
927+
and :attr:`__notes__` but which wraps the exceptions in ``excs``.
919928

920929
This method is used by :meth:`subgroup` and :meth:`split`. A
921930
subclass needs to override it in order to make :meth:`subgroup`

Include/cpython/pyerrors.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
/* PyException_HEAD defines the initial segment of every exception class. */
88
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
9-
PyObject *args; PyObject *note; PyObject *traceback;\
9+
PyObject *args; PyObject *notes; PyObject *traceback;\
1010
PyObject *context; PyObject *cause;\
1111
char suppress_context;
1212

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ struct _Py_global_strings {
147147
STRUCT_FOR_ID(__newobj__)
148148
STRUCT_FOR_ID(__newobj_ex__)
149149
STRUCT_FOR_ID(__next__)
150-
STRUCT_FOR_ID(__note__)
150+
STRUCT_FOR_ID(__notes__)
151151
STRUCT_FOR_ID(__or__)
152152
STRUCT_FOR_ID(__origin__)
153153
STRUCT_FOR_ID(__package__)

Include/internal/pycore_runtime_init.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ extern "C" {
761761
INIT_ID(__newobj__), \
762762
INIT_ID(__newobj_ex__), \
763763
INIT_ID(__next__), \
764-
INIT_ID(__note__), \
764+
INIT_ID(__notes__), \
765765
INIT_ID(__or__), \
766766
INIT_ID(__origin__), \
767767
INIT_ID(__package__), \

Lib/test/test_exception_group.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def leaves(exc):
502502
self.assertIs(eg.__cause__, part.__cause__)
503503
self.assertIs(eg.__context__, part.__context__)
504504
self.assertIs(eg.__traceback__, part.__traceback__)
505-
self.assertIs(eg.__note__, part.__note__)
505+
self.assertEqual(eg.__notes__, part.__notes__)
506506

507507
def tbs_for_leaf(leaf, eg):
508508
for e, tbs in leaf_generator(eg):
@@ -567,7 +567,7 @@ def level3(i):
567567
try:
568568
nested_group()
569569
except ExceptionGroup as e:
570-
e.__note__ = f"the note: {id(e)}"
570+
e.add_note(f"the note: {id(e)}", replace=True)
571571
eg = e
572572

573573
eg_template = [

Lib/test/test_exceptions.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -538,26 +538,40 @@ def testAttributes(self):
538538
'pickled "%r", attribute "%s' %
539539
(e, checkArgName))
540540

541-
def test_note(self):
541+
def test_notes(self):
542542
for e in [BaseException(1), Exception(2), ValueError(3)]:
543543
with self.subTest(e=e):
544-
self.assertIsNone(e.__note__)
545-
e.__note__ = "My Note"
546-
self.assertEqual(e.__note__, "My Note")
544+
self.assertEqual(e.__notes__, ())
545+
e.add_note("My Note")
546+
self.assertEqual(e.__notes__, ("My Note",))
547547

548548
with self.assertRaises(TypeError):
549-
e.__note__ = 42
550-
self.assertEqual(e.__note__, "My Note")
549+
e.add_note(42)
550+
self.assertEqual(e.__notes__, ("My Note",))
551551

552-
e.__note__ = "Your Note"
553-
self.assertEqual(e.__note__, "Your Note")
552+
e.add_note("Your Note")
553+
self.assertEqual(e.__notes__, ("My Note", "Your Note"))
554554

555-
with self.assertRaises(TypeError):
556-
del e.__note__
557-
self.assertEqual(e.__note__, "Your Note")
555+
with self.assertRaises(AttributeError):
556+
e.__notes__ = ("NewNote",)
557+
self.assertEqual(e.__notes__, ("My Note", "Your Note"))
558+
559+
with self.assertRaises(AttributeError):
560+
del e.__notes__
561+
self.assertEqual(e.__notes__, ("My Note", "Your Note"))
562+
563+
e.add_note("Our Note", replace=True)
564+
self.assertEqual(e.__notes__, ("Our Note",))
565+
566+
e.add_note("Our Final Note", replace=False)
567+
self.assertEqual(e.__notes__, ("Our Note", "Our Final Note"))
568+
569+
e.add_note(None, replace=False)
570+
self.assertEqual(e.__notes__, ("Our Note", "Our Final Note"))
571+
572+
e.add_note(None, replace=True)
573+
self.assertEqual(e.__notes__, ())
558574

559-
e.__note__ = None
560-
self.assertIsNone(e.__note__)
561575

562576
def testWithTraceback(self):
563577
try:

Lib/test/test_traceback.py

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,18 +1326,38 @@ def test_exception_with_note(self):
13261326
e = ValueError(42)
13271327
vanilla = self.get_report(e)
13281328

1329-
e.__note__ = 'My Note'
1329+
e.add_note('My Note', replace=True)
13301330
self.assertEqual(self.get_report(e), vanilla + 'My Note\n')
13311331

1332-
e.__note__ = ''
1332+
e.add_note('', replace=True)
13331333
self.assertEqual(self.get_report(e), vanilla + '\n')
13341334

1335-
e.__note__ = 'Your Note'
1335+
e.add_note('Your Note', replace=True)
13361336
self.assertEqual(self.get_report(e), vanilla + 'Your Note\n')
13371337

1338-
e.__note__ = None
1338+
e.add_note(None, replace=True)
13391339
self.assertEqual(self.get_report(e), vanilla)
13401340

1341+
def test_exception_with_note_with_multiple_notes(self):
1342+
e = ValueError(42)
1343+
vanilla = self.get_report(e)
1344+
1345+
e.add_note('Note 1')
1346+
e.add_note('Note 2')
1347+
e.add_note('Note 3')
1348+
1349+
self.assertEqual(
1350+
self.get_report(e),
1351+
vanilla + 'Note 1\n' + 'Note 2\n' + 'Note 3\n')
1352+
1353+
e.add_note('Note 4', replace=True)
1354+
e.add_note('Note 5', replace=True)
1355+
e.add_note('Note 6')
1356+
1357+
self.assertEqual(
1358+
self.get_report(e),
1359+
vanilla + 'Note 5\n' + 'Note 6\n')
1360+
13411361
def test_exception_qualname(self):
13421362
class A:
13431363
class B:
@@ -1688,16 +1708,16 @@ def exc():
16881708
try:
16891709
raise ValueError(msg)
16901710
except ValueError as e:
1691-
e.__note__ = f'the {msg}'
1711+
e.add_note(f'the {msg}')
16921712
excs.append(e)
16931713
raise ExceptionGroup("nested", excs)
16941714
except ExceptionGroup as e:
1695-
e.__note__ = ('>> Multi line note\n'
1696-
'>> Because I am such\n'
1697-
'>> an important exception.\n'
1698-
'>> empty lines work too\n'
1699-
'\n'
1700-
'(that was an empty line)')
1715+
e.add_note(('>> Multi line note\n'
1716+
'>> Because I am such\n'
1717+
'>> an important exception.\n'
1718+
'>> empty lines work too\n'
1719+
'\n'
1720+
'(that was an empty line)'))
17011721
raise
17021722

17031723
expected = (f' + Exception Group Traceback (most recent call last):\n'
@@ -1733,6 +1753,64 @@ def exc():
17331753
report = self.get_report(exc)
17341754
self.assertEqual(report, expected)
17351755

1756+
def test_exception_group_with_multiple_notes(self):
1757+
def exc():
1758+
try:
1759+
excs = []
1760+
for msg in ['bad value', 'terrible value']:
1761+
try:
1762+
raise ValueError(msg)
1763+
except ValueError as e:
1764+
e.add_note(f'the {msg}')
1765+
e.add_note(f'Goodbye {msg}')
1766+
excs.append(e)
1767+
raise ExceptionGroup("nested", excs)
1768+
except ExceptionGroup as e:
1769+
e.add_note(('>> Multi line note\n'
1770+
'>> Because I am such\n'
1771+
'>> an important exception.\n'
1772+
'>> empty lines work too\n'
1773+
'\n'
1774+
'(that was an empty line)'))
1775+
e.add_note('Goodbye!')
1776+
raise
1777+
1778+
expected = (f' + Exception Group Traceback (most recent call last):\n'
1779+
f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
1780+
f' | exception_or_callable()\n'
1781+
f' | ^^^^^^^^^^^^^^^^^^^^^^^\n'
1782+
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n'
1783+
f' | raise ExceptionGroup("nested", excs)\n'
1784+
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
1785+
f' | ExceptionGroup: nested\n'
1786+
f' | >> Multi line note\n'
1787+
f' | >> Because I am such\n'
1788+
f' | >> an important exception.\n'
1789+
f' | >> empty lines work too\n'
1790+
f' | \n'
1791+
f' | (that was an empty line)\n'
1792+
f' | Goodbye!\n'
1793+
f' +-+---------------- 1 ----------------\n'
1794+
f' | Traceback (most recent call last):\n'
1795+
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n'
1796+
f' | raise ValueError(msg)\n'
1797+
f' | ^^^^^^^^^^^^^^^^^^^^^\n'
1798+
f' | ValueError: bad value\n'
1799+
f' | the bad value\n'
1800+
f' | Goodbye bad value\n'
1801+
f' +---------------- 2 ----------------\n'
1802+
f' | Traceback (most recent call last):\n'
1803+
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n'
1804+
f' | raise ValueError(msg)\n'
1805+
f' | ^^^^^^^^^^^^^^^^^^^^^\n'
1806+
f' | ValueError: terrible value\n'
1807+
f' | the terrible value\n'
1808+
f' | Goodbye terrible value\n'
1809+
f' +------------------------------------\n')
1810+
1811+
report = self.get_report(exc)
1812+
self.assertEqual(report, expected)
1813+
17361814

17371815
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
17381816
#

Lib/traceback.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
689689
# Capture now to permit freeing resources: only complication is in the
690690
# unofficial API _format_final_exc_line
691691
self._str = _some_str(exc_value)
692-
self.__note__ = exc_value.__note__ if exc_value else None
692+
self.__notes__ = exc_value.__notes__ if exc_value else None
693693

694694
if exc_type and issubclass(exc_type, SyntaxError):
695695
# Handle SyntaxError's specially
@@ -822,8 +822,9 @@ def format_exception_only(self):
822822
yield _format_final_exc_line(stype, self._str)
823823
else:
824824
yield from self._format_syntax_error(stype)
825-
if self.__note__ is not None:
826-
yield from [l + '\n' for l in self.__note__.split('\n')]
825+
if self.__notes__ is not None:
826+
for note in self.__notes__:
827+
yield from [l + '\n' for l in note.split('\n')]
827828

828829
def _format_syntax_error(self, stype):
829830
"""Format SyntaxError exceptions (internal helper)."""

0 commit comments

Comments
 (0)