mirror of
https://github.com/python/cpython.git
synced 2024-11-24 00:38:00 +01:00
parent
7fa3a5a219
commit
d4c4a76ed1
@ -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`
|
||||
|
@ -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
|
||||
======================
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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__)
|
||||
|
@ -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__), \
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -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.
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user