[Python-checkins] gh-89770: Implement PEP-678 - Exception notes (GH-31317)
iritkatriel
webhook-mailer at python.org
Sat Apr 16 15:00:13 EDT 2022
https://github.com/python/cpython/commit/d4c4a76ed1427c947fcbbe692625b3f644cf3aaf
commit: d4c4a76ed1427c947fcbbe692625b3f644cf3aaf
branch: main
author: Irit Katriel <1055913+iritkatriel at users.noreply.github.com>
committer: iritkatriel <1055913+iritkatriel at users.noreply.github.com>
date: 2022-04-16T19:59:52+01:00
summary:
gh-89770: Implement PEP-678 - Exception notes (GH-31317)
files:
A Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst
M Doc/library/exceptions.rst
M Doc/whatsnew/3.11.rst
M Include/cpython/pyerrors.h
M Include/internal/pycore_global_strings.h
M Include/internal/pycore_runtime_init.h
M Lib/test/test_exception_group.py
M Lib/test/test_exceptions.py
M Lib/test/test_traceback.py
M Lib/traceback.py
M Objects/exceptions.c
M Python/pythonrun.c
diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst
index 137566e079d20..2eccbd17c482c 100644
--- a/Doc/library/exceptions.rst
+++ b/Doc/library/exceptions.rst
@@ -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
@@ -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,
@@ -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`
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 19e3d2f92fb94..cd452fc537157 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -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
======================
diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h
index 08630cce8ac90..47d80e3242302 100644
--- a/Include/cpython/pyerrors.h
+++ b/Include/cpython/pyerrors.h
@@ -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;
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index 833ff2710a787..77f96cd5ed2f0 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -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__)
diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h
index fd925b3e060df..371f2d23ad8ec 100644
--- a/Include/internal/pycore_runtime_init.h
+++ b/Include/internal/pycore_runtime_init.h
@@ -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__), \
diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py
index 793e8d20de7e3..2cfd8738304d1 100644
--- a/Lib/test/test_exception_group.py
+++ b/Lib/test/test_exception_group.py
@@ -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):
@@ -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 = [
@@ -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):
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 6dca79efef180..2b5b51934562a 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -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:
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 75d668df64d4c..962322c89ff66 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -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:
@@ -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'
@@ -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 (2 sub-exceptions)\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):
#
@@ -2077,32 +2194,32 @@ def some_inner():
[f'{__file__}:{some_inner.__code__.co_firstlineno + 1}'])
def test_dropping_frames(self):
- def f():
- 1/0
-
- def g():
- try:
- f()
- except:
- return sys.exc_info()
-
- exc_info = g()
-
- class Skip_G(traceback.StackSummary):
- def format_frame_summary(self, frame_summary):
- if frame_summary.name == 'g':
- return None
- return super().format_frame_summary(frame_summary)
-
- stack = Skip_G.extract(
- traceback.walk_tb(exc_info[2])).format()
-
- self.assertEqual(len(stack), 1)
- lno = f.__code__.co_firstlineno + 1
- self.assertEqual(
- stack[0],
- f' File "{__file__}", line {lno}, in f\n 1/0\n'
- )
+ def f():
+ 1/0
+
+ def g():
+ try:
+ f()
+ except:
+ return sys.exc_info()
+
+ exc_info = g()
+
+ class Skip_G(traceback.StackSummary):
+ def format_frame_summary(self, frame_summary):
+ if frame_summary.name == 'g':
+ return None
+ return super().format_frame_summary(frame_summary)
+
+ stack = Skip_G.extract(
+ traceback.walk_tb(exc_info[2])).format()
+
+ self.assertEqual(len(stack), 1)
+ lno = f.__code__.co_firstlineno + 1
+ self.assertEqual(
+ stack[0],
+ f' File "{__file__}", line {lno}, in f\n 1/0\n'
+ )
class TestTracebackException(unittest.TestCase):
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 05f1fffef0d3b..3afe49d1d8a0e 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -1,6 +1,6 @@
"""Extract, format and print information about Python stack traces."""
-import collections
+import collections.abc
import itertools
import linecache
import sys
@@ -163,18 +163,18 @@ def format_exception_only(exc, /, value=_sentinel):
# -- not official API but folk probably use these two functions.
def _format_final_exc_line(etype, value):
- valuestr = _some_str(value)
+ valuestr = _safe_string(value, 'exception')
if value is None or not valuestr:
line = "%s\n" % etype
else:
line = "%s: %s\n" % (etype, valuestr)
return line
-def _some_str(value):
+def _safe_string(value, what, func=str):
try:
- return str(value)
+ return func(value)
except:
- return '<exception str() failed>'
+ return f'<{what} {func.__name__}() failed>'
# --
@@ -688,8 +688,8 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self.exc_type = exc_type
# 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._str = _safe_string(exc_value, 'exception')
+ self.__notes__ = getattr(exc_value, '__notes__', None)
if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially
@@ -822,8 +822,12 @@ 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__:
+ note = _safe_string(note, 'note')
+ yield from [l + '\n' for l in note.split('\n')]
+ elif self.__notes__ is not None:
+ yield _safe_string(self.__notes__, '__notes__', func=repr)
def _format_syntax_error(self, stype):
"""Format SyntaxError exceptions (internal helper)."""
@@ -913,7 +917,7 @@ def format(self, *, chain=True, _ctx=None):
# format exception group
is_toplevel = (_ctx.exception_group_depth == 0)
if is_toplevel:
- _ctx.exception_group_depth += 1
+ _ctx.exception_group_depth += 1
if exc.stack:
yield from _ctx.emit(
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst b/Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst
new file mode 100644
index 0000000000000..e131e91e75312
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-04-12-11-56-23.gh-issue-91479.-dyGJX.rst
@@ -0,0 +1 @@
+Replaced the ``__note__`` field of :exc:`BaseException` (added in an earlier version of 3.11) with the final design of :pep:`678`. Namely, :exc:`BaseException` gets an :meth:`add_note` method, and its ``__notes__`` field is created when necessary.
diff --git a/Objects/exceptions.c b/Objects/exceptions.c
index df10a3c2416e3..b26a0e93af484 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -47,7 +47,7 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
return NULL;
/* the dict is created on the fly in PyObject_GenericSetAttr */
self->dict = NULL;
- self->note = NULL;
+ self->notes = NULL;
self->traceback = self->cause = self->context = NULL;
self->suppress_context = 0;
@@ -83,7 +83,7 @@ BaseException_clear(PyBaseExceptionObject *self)
{
Py_CLEAR(self->dict);
Py_CLEAR(self->args);
- Py_CLEAR(self->note);
+ Py_CLEAR(self->notes);
Py_CLEAR(self->traceback);
Py_CLEAR(self->cause);
Py_CLEAR(self->context);
@@ -108,7 +108,7 @@ BaseException_traverse(PyBaseExceptionObject *self, visitproc visit, void *arg)
{
Py_VISIT(self->dict);
Py_VISIT(self->args);
- Py_VISIT(self->note);
+ Py_VISIT(self->notes);
Py_VISIT(self->traceback);
Py_VISIT(self->cause);
Py_VISIT(self->context);
@@ -186,12 +186,62 @@ PyDoc_STRVAR(with_traceback_doc,
"Exception.with_traceback(tb) --\n\
set self.__traceback__ to tb and return self.");
+static inline PyBaseExceptionObject*
+_PyBaseExceptionObject_cast(PyObject *exc)
+{
+ assert(PyExceptionInstance_Check(exc));
+ return (PyBaseExceptionObject *)exc;
+}
+
+static PyObject *
+BaseException_add_note(PyObject *self, PyObject *note)
+{
+ if (!PyUnicode_Check(note)) {
+ PyErr_Format(PyExc_TypeError,
+ "note must be a str, not '%s'",
+ Py_TYPE(note)->tp_name);
+ return NULL;
+ }
+
+ if (!PyObject_HasAttr(self, &_Py_ID(__notes__))) {
+ PyObject *new_notes = PyList_New(0);
+ if (new_notes == NULL) {
+ return NULL;
+ }
+ if (PyObject_SetAttr(self, &_Py_ID(__notes__), new_notes) < 0) {
+ Py_DECREF(new_notes);
+ return NULL;
+ }
+ Py_DECREF(new_notes);
+ }
+ PyObject *notes = PyObject_GetAttr(self, &_Py_ID(__notes__));
+ if (notes == NULL) {
+ return NULL;
+ }
+ if (!PyList_Check(notes)) {
+ Py_DECREF(notes);
+ PyErr_SetString(PyExc_TypeError, "Cannot add note: __notes__ is not a list");
+ return NULL;
+ }
+ if (PyList_Append(notes, note) < 0) {
+ Py_DECREF(notes);
+ return NULL;
+ }
+ Py_DECREF(notes);
+ Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(add_note_doc,
+"Exception.add_note(note) --\n\
+ add a note to the exception");
static PyMethodDef BaseException_methods[] = {
{"__reduce__", (PyCFunction)BaseException_reduce, METH_NOARGS },
{"__setstate__", (PyCFunction)BaseException_setstate, METH_O },
{"with_traceback", (PyCFunction)BaseException_with_traceback, METH_O,
with_traceback_doc},
+ {"add_note", (PyCFunction)BaseException_add_note, METH_O,
+ add_note_doc},
{NULL, NULL, 0, NULL},
};
@@ -220,33 +270,6 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS
return 0;
}
-static PyObject *
-BaseException_get_note(PyBaseExceptionObject *self, void *Py_UNUSED(ignored))
-{
- if (self->note == NULL) {
- Py_RETURN_NONE;
- }
- return Py_NewRef(self->note);
-}
-
-static int
-BaseException_set_note(PyBaseExceptionObject *self, PyObject *note,
- void *Py_UNUSED(ignored))
-{
- if (note == NULL) {
- PyErr_SetString(PyExc_TypeError, "__note__ may not be deleted");
- return -1;
- }
- else if (note != Py_None && !PyUnicode_CheckExact(note)) {
- PyErr_SetString(PyExc_TypeError, "__note__ must be a string or None");
- return -1;
- }
-
- Py_INCREF(note);
- Py_XSETREF(self->note, note);
- return 0;
-}
-
static PyObject *
BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored))
{
@@ -337,7 +360,6 @@ BaseException_set_cause(PyObject *self, PyObject *arg, void *Py_UNUSED(ignored))
static PyGetSetDef BaseException_getset[] = {
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
{"args", (getter)BaseException_get_args, (setter)BaseException_set_args},
- {"__note__", (getter)BaseException_get_note, (setter)BaseException_set_note},
{"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb},
{"__context__", BaseException_get_context,
BaseException_set_context, PyDoc_STR("exception context")},
@@ -347,14 +369,6 @@ static PyGetSetDef BaseException_getset[] = {
};
-static inline PyBaseExceptionObject*
-_PyBaseExceptionObject_cast(PyObject *exc)
-{
- assert(PyExceptionInstance_Check(exc));
- return (PyBaseExceptionObject *)exc;
-}
-
-
PyObject *
PyException_GetTraceback(PyObject *self)
{
@@ -910,9 +924,32 @@ exceptiongroup_subset(
PyException_SetContext(eg, PyException_GetContext(orig));
PyException_SetCause(eg, PyException_GetCause(orig));
- PyObject *note = _PyBaseExceptionObject_cast(orig)->note;
- Py_XINCREF(note);
- _PyBaseExceptionObject_cast(eg)->note = note;
+ if (PyObject_HasAttr(orig, &_Py_ID(__notes__))) {
+ PyObject *notes = PyObject_GetAttr(orig, &_Py_ID(__notes__));
+ if (notes == NULL) {
+ goto error;
+ }
+ if (PySequence_Check(notes)) {
+ /* Make a copy so the parts have independent notes lists. */
+ PyObject *notes_copy = PySequence_List(notes);
+ Py_DECREF(notes);
+ if (notes_copy == NULL) {
+ goto error;
+ }
+ int res = PyObject_SetAttr(eg, &_Py_ID(__notes__), notes_copy);
+ Py_DECREF(notes_copy);
+ if (res < 0) {
+ goto error;
+ }
+ }
+ else {
+ /* __notes__ is supposed to be a list, and split() is not a
+ * good place to report earlier user errors, so we just ignore
+ * notes of non-sequence type.
+ */
+ Py_DECREF(notes);
+ }
+ }
*result = eg;
return 0;
@@ -1262,7 +1299,7 @@ is_same_exception_metadata(PyObject *exc1, PyObject *exc2)
PyBaseExceptionObject *e1 = (PyBaseExceptionObject *)exc1;
PyBaseExceptionObject *e2 = (PyBaseExceptionObject *)exc2;
- return (e1->note == e2->note &&
+ return (e1->notes == e2->notes &&
e1->traceback == e2->traceback &&
e1->cause == e2->cause &&
e1->context == e2->context);
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index e086f0f345c22..769c34ea161e0 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -1129,7 +1129,7 @@ print_exception_suggestions(struct exception_print_context *ctx,
}
static int
-print_exception_note(struct exception_print_context *ctx, PyObject *value)
+print_exception_notes(struct exception_print_context *ctx, PyObject *value)
{
PyObject *f = ctx->file;
@@ -1137,41 +1137,74 @@ print_exception_note(struct exception_print_context *ctx, PyObject *value)
return 0;
}
- PyObject *note = PyObject_GetAttr(value, &_Py_ID(__note__));
- if (note == NULL) {
+ if (!PyObject_HasAttr(value, &_Py_ID(__notes__))) {
+ return 0;
+ }
+ PyObject *notes = PyObject_GetAttr(value, &_Py_ID(__notes__));
+ if (notes == NULL) {
return -1;
}
- if (!PyUnicode_Check(note)) {
- Py_DECREF(note);
- return 0;
+ if (!PySequence_Check(notes)) {
+ int res = 0;
+ if (write_indented_margin(ctx, f) < 0) {
+ res = -1;
+ }
+ PyObject *s = PyObject_Repr(notes);
+ if (s == NULL) {
+ PyErr_Clear();
+ res = PyFile_WriteString("<__notes__ repr() failed>", f);
+ }
+ else {
+ res = PyFile_WriteObject(s, f, Py_PRINT_RAW);
+ Py_DECREF(s);
+ }
+ Py_DECREF(notes);
+ return res;
}
+ Py_ssize_t num_notes = PySequence_Length(notes);
+ PyObject *lines = NULL;
+ for (Py_ssize_t ni = 0; ni < num_notes; ni++) {
+ PyObject *note = PySequence_GetItem(notes, ni);
+ PyObject *note_str = PyObject_Str(note);
+ Py_DECREF(note);
- PyObject *lines = PyUnicode_Splitlines(note, 1);
- Py_DECREF(note);
+ if (note_str == NULL) {
+ PyErr_Clear();
+ if (PyFile_WriteString("<note str() failed>", f) < 0) {
+ goto error;
+ }
+ }
+ else {
+ lines = PyUnicode_Splitlines(note_str, 1);
+ Py_DECREF(note_str);
- if (lines == NULL) {
- return -1;
- }
+ if (lines == NULL) {
+ goto error;
+ }
- Py_ssize_t n = PyList_GET_SIZE(lines);
- for (Py_ssize_t i = 0; i < n; i++) {
- PyObject *line = PyList_GET_ITEM(lines, i);
- assert(PyUnicode_Check(line));
- if (write_indented_margin(ctx, f) < 0) {
- goto error;
+ Py_ssize_t n = PyList_GET_SIZE(lines);
+ for (Py_ssize_t i = 0; i < n; i++) {
+ PyObject *line = PyList_GET_ITEM(lines, i);
+ assert(PyUnicode_Check(line));
+ if (write_indented_margin(ctx, f) < 0) {
+ goto error;
+ }
+ if (PyFile_WriteObject(line, f, Py_PRINT_RAW) < 0) {
+ goto error;
+ }
+ }
+ Py_CLEAR(lines);
}
- if (PyFile_WriteObject(line, f, Py_PRINT_RAW) < 0) {
+ if (PyFile_WriteString("\n", f) < 0) {
goto error;
}
}
- if (PyFile_WriteString("\n", f) < 0) {
- goto error;
- }
- Py_DECREF(lines);
+ Py_DECREF(notes);
return 0;
error:
- Py_DECREF(lines);
+ Py_XDECREF(lines);
+ Py_DECREF(notes);
return -1;
}
@@ -1206,7 +1239,7 @@ print_exception(struct exception_print_context *ctx, PyObject *value)
if (PyFile_WriteString("\n", f) < 0) {
goto error;
}
- if (print_exception_note(ctx, value) < 0) {
+ if (print_exception_notes(ctx, value) < 0) {
goto error;
}
More information about the Python-checkins
mailing list