0
0
mirror of https://github.com/python/cpython.git synced 2024-11-24 00:38:00 +01:00

gh-89770: Implement PEP-678 - Exception notes (GH-31317)

This commit is contained in:
Irit Katriel 2022-04-16 19:59:52 +01:00 committed by GitHub
parent 7fa3a5a219
commit d4c4a76ed1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 383 additions and 144 deletions

View File

@ -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`

View File

@ -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
======================

View File

@ -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;

View File

@ -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__)

View File

@ -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__), \

View File

@ -567,7 +567,9 @@ class ExceptionGroupSplitTestBase(ExceptionGroupTestBase):
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 @@ class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase):
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 @@ class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase):
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):

View File

@ -547,26 +547,32 @@ class ExceptionTests(unittest.TestCase):
'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"])
del e.__notes__
self.assertFalse(hasattr(e, '__notes__'))
e.add_note("Our Note")
self.assertEqual(e.__notes__, ["Our Note"])
e.__notes__ = 42
self.assertEqual(e.__notes__, 42)
with self.assertRaises(TypeError):
del e.__note__
self.assertEqual(e.__note__, "Your Note")
e.__note__ = None
self.assertIsNone(e.__note__)
e.add_note("will not work")
self.assertEqual(e.__notes__, 42)
def testWithTraceback(self):
try:

View File

@ -1323,20 +1323,79 @@ class BaseExceptionReportingTests:
self.assertEqual(exp, err)
def test_exception_with_note(self):
e = ValueError(123)
vanilla = self.get_report(e)
e.add_note('My Note')
self.assertEqual(self.get_report(e), vanilla + 'My Note\n')
del e.__notes__
e.add_note('')
self.assertEqual(self.get_report(e), vanilla + '\n')
del e.__notes__
e.add_note('Your Note')
self.assertEqual(self.get_report(e), vanilla + 'Your Note\n')
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.__note__ = 'My Note'
self.assertEqual(self.get_report(e), vanilla + 'My Note\n')
e.add_note('Note 1')
e.add_note('Note 2')
e.add_note('Note 3')
e.__note__ = ''
self.assertEqual(self.get_report(e), vanilla + '\n')
self.assertEqual(
self.get_report(e),
vanilla + 'Note 1\n' + 'Note 2\n' + 'Note 3\n')
e.__note__ = 'Your Note'
self.assertEqual(self.get_report(e), vanilla + 'Your Note\n')
del e.__notes__
e.add_note('Note 4')
del e.__notes__
e.add_note('Note 5')
e.add_note('Note 6')
e.__note__ = None
self.assertEqual(self.get_report(e), vanilla)
self.assertEqual(
self.get_report(e),
vanilla + 'Note 5\n' + 'Note 6\n')
def test_exception_qualname(self):
class A:
@ -1688,16 +1747,16 @@ class BaseExceptionReportingTests:
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 @@ class BaseExceptionReportingTests:
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 @@ class TestStack(unittest.TestCase):
[f'{__file__}:{some_inner.__code__.co_firstlineno + 1}'])
def test_dropping_frames(self):
def f():
1/0
def f():
1/0
def g():
try:
f()
except:
return sys.exc_info()
def g():
try:
f()
except:
return sys.exc_info()
exc_info = g()
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)
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()
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'
)
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):

View File

@ -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 @@ class TracebackException:
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 @@ class TracebackException:
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 @@ class TracebackException:
# 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(

View File

@ -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.

View File

@ -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);

View File

@ -1129,7 +1129,7 @@ error:
}
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) {
return -1;
}
if (!PyUnicode_Check(note)) {
Py_DECREF(note);
if (!PyObject_HasAttr(value, &_Py_ID(__notes__))) {
return 0;
}
PyObject *lines = PyUnicode_Splitlines(note, 1);
Py_DECREF(note);
if (lines == NULL) {
PyObject *notes = PyObject_GetAttr(value, &_Py_ID(__notes__));
if (notes == NULL) {
return -1;
}
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 (!PySequence_Check(notes)) {
int res = 0;
if (write_indented_margin(ctx, f) < 0) {
goto error;
res = -1;
}
if (PyFile_WriteObject(line, f, Py_PRINT_RAW) < 0) {
goto error;
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;
}
if (PyFile_WriteString("\n", f) < 0) {
goto error;
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);
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) {
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_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;
}