8000 gh-89770: Implement PEP-678 - Exception notes (GH-31317) · python/cpython@d4c4a76 · GitHub
[go: up one dir, main page]

Skip to content

Commit d4c4a76

Browse files
authored
gh-89770: Implement PEP-678 - Exception notes (GH-31317)
1 parent 7fa3a5a commit d4c4a76

File tree

12 files changed

+384
-145
lines changed

12 files changed

+384
-145
lines changed

Doc/library/exceptions.rst

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,20 @@ 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)
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+
Add the string ``note`` to the exception's notes which appear in the standard
132+
traceback after the exception string. A :exc:`TypeError` is raised if ``note``
133+
is not a string.
134134

135-
.. versionadded:: 3.11
135+
.. versionadded:: 3.11
136+
137+
.. attribute:: __notes__
138+
139+
A list of the notes of this exception, which were added with :meth:`add_note`.
140+
This attribute is created when :meth:`add_note` is called.
141+
142+
.. versionadded:: 3.11
136143

137144

138145
.. exception:: Exception
@@ -907,7 +914,7 @@ their subgroups based on the types of the contained exceptions.
907914

908915
The nesting structure of the current exception is preserved in the result,
909916
as a 8000 re the values of its :attr:`message`, :attr:`__traceback__`,
910-
:attr:`__cause__`, :attr:`__context__` and :attr:`__note__` fields.
917+
:attr:`__cause__`, :attr:`__context__` and :attr:`__notes__` fields.
911918
Empty nested groups are omitted from the result.
912919

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

925932
Returns an exception group with the same :attr:`message`,
926933
:attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__`
927-
and :attr:`__note__` but which wraps the exceptions in ``excs``.
934+
and :attr:`__notes__` but which wraps the exceptions in ``excs``.
928935

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

Doc/whatsnew/3.11.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,15 @@ The :option:`-X` ``no_debug_ranges`` option and the environment variable
157157
See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
158158
and Ammar Askar in :issue:`43950`.)
159159

160-
Exceptions can be enriched with a string ``__note__``
161-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
160+
Exceptions can be enriched with notes (PEP 678)
161+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
162+
163+
The :meth:`add_note` method was added to :exc:`BaseException`. It can be
164+
used to enrich exceptions with context information which is not available
165+
at the time when the exception is raised. The notes added appear in the
166+
default traceback. See :pep:`678` for more details. (Contributed by
167+
Irit Katriel in :issue:`45607`.)
162168

163-
The ``__note__`` field was added to :exc:`BaseException`. It is ``None``
164-
by default but can be set to a string which is added to the exception's
165-
traceback. (Contributed by Irit Katriel in :issue:`45607`.)
166169

167170
Other Language Changes
168171
======================

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
@@ -155,7 +155,7 @@ struct _Py_global_strings {
155155
STRUCT_FOR_ID(__newobj__)
156156
STRUCT_FOR_ID(__newobj_ex__)
157157
STRUCT_FOR_ID(__next__)
158-
STRUCT_FOR_ID(__note__)
158+
STRUCT_FOR_ID(__notes__)
159159
STRUCT_FOR_ID(__or__)
160160
STRUCT_FOR_ID(__orig_class__)
161161
STRUCT_FOR_ID(__origin__)

Include/internal/pycore_runtime_init.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ extern "C" {
778778
INIT_ID(__newobj__), \
779779
INIT_ID(__newobj_ex__), \
780780
INIT_ID(__next__), \
781-
INIT_ID(__note__), \
781+
INIT_ID(__notes__), \
782782
INIT_ID(__or__), \
783783
INIT_ID(__orig_class__), \
784784
INIT_ID(__origin__), \

Lib/test/test_exception_group.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,9 @@ def leaves(exc):
567567
self.assertIs(eg.__cause__, part.__cause__)
568568
self.assertIs(eg.__context__, part.__context__)
569569
self.assertIs(eg.__traceback__, part.__traceback__)
570-
self.assertIs(eg.__note__, part.__note__)
570+
self.assertEqual(
571+
getattr(eg, '__notes__', None),
572+
getattr(part, '__notes__', None))
571573

572574
def tbs_for_leaf(leaf, eg):
573575
for e, tbs in leaf_generator(eg):
@@ -632,7 +634,7 @@ def level3(i):
632634
try:
633635
nested_group()
634636
except ExceptionGroup as e:
635-
e.__note__ = f"the note: {id(e)}"
637+
e.add_note(f"the note: {id(e)}")
636638
eg = e
637639

638640
eg_template = [
@@ -728,6 +730,35 @@ def exc(ex):
728730
self.assertMatchesTemplate(
729731
rest, ExceptionGroup, [ValueError(1)])
730732

733+
def test_split_copies_notes(self):
734+
# make sure each exception group after a split has its own __notes__ list
735+
eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)])
736+
eg.add_note("note1")
737+
eg.add_note("note2")
738+
orig_notes = list(eg.__notes__)
739+
match, rest = eg.split(TypeError)
740+
self.assertEqual(eg.__notes__, orig_notes)
741+
self.assertEqual(match.__notes__, orig_notes)
742+
self.assertEqual(rest.__notes__, orig_notes)
743+
self.assertIsNot(eg.__notes__, match.__notes__)
744+
self.assertIsNot(eg.__notes__, rest.__notes__)
745+
self.assertIsNot(match.__notes__, rest.__notes__)
746+
eg.add_note("eg")
747+
match.add_note("match")
748+
rest.add_note("rest")
749+
self.assertEqual(eg.__notes__, orig_notes + ["eg"])
750+
self.assertEqual(match.__notes__, orig_notes + ["match"])
751+
self.assertEqual(rest.__notes__, orig_notes + ["rest"])
752+
753+
def test_split_does_not_copy_non_sequence_notes(self):
754+
# __notes__ should be a sequence, which is shallow copied.
755+
# If it is not a sequence, the split parts don't get any notes.
756+
eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)])
757+
eg.__notes__ = 123
758+
match, rest = eg.split(TypeError)
759+
self.assertFalse(hasattr(match, '__notes__'))
760+
self.assertFalse(hasattr(rest, '__notes__'))
761+
731762

732763
class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase):
733764

Lib/test/test_exceptions.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -547,26 +547,32 @@ def testAttributes(self):
547547
'pickled "%r", attribute "%s' %
548548
(e, checkArgName))
549549

550-
def test_note(self):
550+
def test_notes(self):
551551
for e in [BaseException(1), Exception(2), ValueError(3)]:
552552
with self.subTest(e=e):
553-
self.assertIsNone(e.__note__)
554-
e.__note__ = "My Note"
555-
self.assertEqual(e.__note__, "My Note")
553+
self.assertFalse(hasattr(e, '__notes__'))
554+
e.add_note("My Note")
555+
self.assertEqual(e.__notes__, ["My Note"])
556556

557557
with self.assertRaises(TypeError):
558-
e.__note__ = 42
559-
self.assertEqual(e.__note__, "My Note")
558+
e.add_note(42)
559+
self.assertEqual(e.__notes__, ["My Note"])
560560

561-
e.__note__ = "Your Note"
562-
self.assertEqual(e.__note__, "Your Note")
561+
e.add_note("Your Note")
562+
self.assertEqual(e.__notes__, ["My Note", "Your Note"])
563563

564-
with self.assertRaises(TypeError):
565-
del e.__note__
566-
self.assertEqual(e.__note__, "Your Note")
564+
del e.__notes__
565+
self.assertFalse(hasattr(e, '__notes__'))
566+
567+
e.add_note("Our Note")
568+
self.assertEqual(e.__notes__, ["Our Note"])
567569

568-
e.__note__ = None
569-
self.assertIsNone(e.__note__)
570+
e.__notes__ = 42
571+
self.assertEqual(e.__notes__, 42)
572+
573+
with self.assertRaises(TypeError):
574+
e.add_note("will not work")
575+
self.assertEqual(e.__notes__, 42)
570576

571577
def testWithTraceback(self):
572578
try:

0 commit comments

Comments
 (0)
0