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 5 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 tuple of the notes of this exception, which were added with :meth:`add_note`.
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
Its contents can be cleared with ``del e.__notes__``.

.. 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
6 changes: 4 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
28 changes: 15 additions & 13 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,26 +538,28 @@ 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

def testWithTraceback(self):
try:
Expand Down
112 changes: 101 additions & 11 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1326,18 +1326,50 @@ def test_exception_with_note(self):
e = ValueError(42)
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)

# non-sequence __notes__ is ignored
e.__notes__ = 42
self.assertEqual(self.get_report(e), vanilla)

# non-string items in the __notes__ sequence are ignored
e.__notes__ = [42, 'Final Note']
self.assertEqual(self.get_report(e), vanilla + 'Final 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 +1720,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 +1765,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
10 changes: 6 additions & 4 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Extract, format and print information about Python stack traces."""

import collections
import collections.abc
import itertools
import linecache
import sys
Expand Down Expand Up @@ -689,7 +689,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
# 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.__notes__ = getattr(exc_value, '__notes__', None)

if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially
Expand Down Expand Up @@ -822,8 +822,10 @@ def format_exception_only(self):
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__:
if isinstance(note, str):
yield from [l + '\n' for l in note.split('\n')]

def _format_syntax_error(self, stype):
"""Format SyntaxError exceptions (internal helper)."""
Expand Down
Loading