Skip to content

Commit

Permalink
gh-89770: Implement PEP-678 - Exception notes (GH-31317)
Browse files Browse the repository at this point in the history
  • Loading branch information
iritkatriel authored Apr 16, 2022
1 parent 7fa3a5a commit d4c4a76
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 145 deletions.
21 changes: 14 additions & 7 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,20 @@ The following exceptions are used mostly as base classes for other exceptions.
tb = sys.exc_info()[2]
raise OtherException(...).with_traceback(tb)

.. attribute:: __note__
.. method:: add_note(note)

A mutable field which is :const:`None` by default and can be set to a string.
If it is not :const:`None`, it is included in the traceback. This field can
be used to enrich exceptions after they have been caught.
Add the string ``note`` to the exception's notes which appear in the standard
traceback after the exception string. A :exc:`TypeError` is raised if ``note``
is not a string.

.. versionadded:: 3.11
.. versionadded:: 3.11

.. attribute:: __notes__

A list of the notes of this exception, which were added with :meth:`add_note`.
This attribute is created when :meth:`add_note` is called.

.. versionadded:: 3.11


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

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

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

Returns an exception group with the same :attr:`message`,
:attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__`
and :attr:`__note__` but which wraps the exceptions in ``excs``.
and :attr:`__notes__` but which wraps the exceptions in ``excs``.

This method is used by :meth:`subgroup` and :meth:`split`. A
subclass needs to override it in order to make :meth:`subgroup`
Expand Down
13 changes: 8 additions & 5 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,15 @@ The :option:`-X` ``no_debug_ranges`` option and the environment variable
See :pep:`657` for more details. (Contributed by Pablo Galindo, Batuhan Taskaya
and Ammar Askar in :issue:`43950`.)

Exceptions can be enriched with a string ``__note__``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Exceptions can be enriched with notes (PEP 678)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :meth:`add_note` method was added to :exc:`BaseException`. It can be
used to enrich exceptions with context information which is not available
at the time when the exception is raised. The notes added appear in the
default traceback. See :pep:`678` for more details. (Contributed by
Irit Katriel in :issue:`45607`.)

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

Other Language Changes
======================
Expand Down
2 changes: 1 addition & 1 deletion Include/cpython/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/* PyException_HEAD defines the initial segment of every exception class. */
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
PyObject *args; PyObject *note; PyObject *traceback;\
PyObject *args; PyObject *notes; PyObject *traceback;\
PyObject *context; PyObject *cause;\
char suppress_context;

Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__newobj__)
STRUCT_FOR_ID(__newobj_ex__)
STRUCT_FOR_ID(__next__)
STRUCT_FOR_ID(__note__)
STRUCT_FOR_ID(__notes__)
STRUCT_FOR_ID(__or__)
STRUCT_FOR_ID(__orig_class__)
STRUCT_FOR_ID(__origin__)
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ extern "C" {
INIT_ID(__newobj__), \
INIT_ID(__newobj_ex__), \
INIT_ID(__next__), \
INIT_ID(__note__), \
INIT_ID(__notes__), \
INIT_ID(__or__), \
INIT_ID(__orig_class__), \
INIT_ID(__origin__), \
Expand Down
35 changes: 33 additions & 2 deletions Lib/test/test_exception_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,9 @@ def leaves(exc):
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):
Expand Down Expand Up @@ -632,7 +634,7 @@ def level3(i):
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 = [
Expand Down Expand Up @@ -728,6 +730,35 @@ def exc(ex):
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):

Expand Down
32 changes: 19 additions & 13 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,26 +547,32 @@ def testAttributes(self):
'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:
Expand Down
Loading

0 comments on commit d4c4a76

Please sign in to comment.