Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-89770: Implement PEP-678 - Exception notes #31317

Merged
merged 23 commits into from
Apr 16, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1555087
PEP-678: exception notes are set by add_note(). __notes__ holds a tup…
iritkatriel Feb 13, 2022
6a38688
clear notes with del e.__notes__, remove replace arg and None note op…
iritkatriel Feb 23, 2022
7bab63e
do not create new tuple when __notes__ is accessed
iritkatriel Mar 2, 2022
396b5f1
Revert "do not create new tuple when __notes__ is accessed"
iritkatriel Mar 16, 2022
f518aa5
add __notes__ attribute in add_note. Traceback ignores it if it's not…
iritkatriel Mar 16, 2022
0438933
add in add_note a check that __notes__ is a list. Add the test. tweak…
iritkatriel Mar 16, 2022
dcc93ef
TypeError in add_note if __notes__ is not a list
iritkatriel Mar 16, 2022
614378e
if __notes__ is not sequence, print repr(__notes__). If note is not a…
iritkatriel Mar 17, 2022
f240e71
simplify traceback code (no need to special case note which is a string)
iritkatriel Mar 21, 2022
6786dbd
shallow copy the notes in split(), if it's a sequence
iritkatriel Mar 22, 2022
bdd4e2a
split() ignores notes if they are not a sequence
iritkatriel Mar 23, 2022
711e804
typo in doc
iritkatriel Mar 28, 2022
e147f52
fix typo in test
iritkatriel Apr 12, 2022
622ca51
Merge remote-tracking branch 'upstream/main' into pep-678
iritkatriel Apr 12, 2022
404f80d
📜🤖 Added by blurb_it.
blurb-it[bot] Apr 12, 2022
c014ad8
update test_traceback
iritkatriel Apr 12, 2022
47bfcdc
Merge branch 'main' into pep-678
iritkatriel Apr 12, 2022
2ae22e4
update whatnew
iritkatriel Apr 12, 2022
95be670
METH_VARARGS --> METH_O
iritkatriel Apr 14, 2022
520efd1
Merge branch 'main' into pep-678
iritkatriel Apr 14, 2022
a14e915
add_note no longer has a replace kwarg
iritkatriel Apr 14, 2022
991e982
finish converting add_note to METH_O
iritkatriel Apr 14, 2022
602b4c4
fix whitespace
iritkatriel Apr 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 raise if ``note``
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -898,7 +905,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 @@ -915,7 +922,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
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 @@ -147,7 +147,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(__origin__)
STRUCT_FOR_ID(__package__)
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 @@ -761,7 +761,7 @@ extern "C" {
INIT_ID(__newobj__), \
INIT_ID(__newobj_ex__), \
INIT_ID(__next__), \
INIT_ID(__note__), \
INIT_ID(__notes__), \
INIT_ID(__or__), \
INIT_ID(__origin__), \
INIT_ID(__package__), \
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 @@ -502,7 +502,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 @@ -567,7 +569,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 @@ -663,6 +665,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
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -538,26 +538,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)
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved

with self.assertRaises(TypeError):
e.add_note("will not work")
self.assertEqual(e.__notes__, 42)

def testWithTraceback(self):
try:
Expand Down
141 changes: 129 additions & 12 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,21 +1323,80 @@ def test_syntax_error_various_offsets(self):
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:
Expand Down Expand Up @@ -1688,16 +1747,16 @@ def exc():
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'
Expand Down Expand Up @@ -1733,6 +1792,64 @@ def exc():
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\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):
#
Expand Down
Loading