Skip to content

bpo-45711: Change exc_info related APIs to derive type and traceback from the exception instance #29780

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

Merged
merged 9 commits into from
Nov 30, 2021
7 changes: 6 additions & 1 deletion Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,6 @@ Querying the error indicator
to an exception that was *already caught*, not to an exception that was
freshly raised. This function steals the references of the arguments.
To clear the exception state, pass ``NULL`` for all three arguments.
For general rules about the three arguments, see :c:func:`PyErr_Restore`.

.. note::

Expand All @@ -493,6 +492,12 @@ Querying the error indicator

.. versionadded:: 3.3

.. versionchanged:: 3.11
The ``type`` and ``traceback`` arguments are no longer used and
can be NULL. The interpreter now derives them from the exception
instance (the ``value`` argument). The function still steals
references of all three arguments.


Signal Handling
===============
Expand Down
11 changes: 8 additions & 3 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,14 @@ always available.
``(type, value, traceback)``. Their meaning is: *type* gets the type of the
exception being handled (a subclass of :exc:`BaseException`); *value* gets
the exception instance (an instance of the exception type); *traceback* gets
a :ref:`traceback object <traceback-objects>` which encapsulates the call
stack at the point where the exception originally occurred.

a :ref:`traceback object <traceback-objects>` which typically encapsulates
the call stack at the point where the exception last occurred.

.. versionchanged:: 3.11
The ``type`` and ``traceback`` fields are now derived from the ``value``
(the exception instance), so when an exception is modified while it is
being handled, the changes are reflected in the results of subsequent
calls to :func:`exc_info`.

.. data:: exec_prefix

Expand Down
6 changes: 6 additions & 0 deletions Doc/reference/simple_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,12 @@ and information about handling exceptions is in section :ref:`try`.
The ``__suppress_context__`` attribute to suppress automatic display of the
exception context.

.. versionchanged:: 3.11
If the traceback of the active exception is modified in an :keyword:`except`
clause, a subsequent ``raise`` statement re-raises the exception with the
modified traceback. Previously, the exception was re-raised with the
traceback it had when it was caught.
Comment on lines +661 to +662
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, the exception was re-raised with the traceback it had when it was caught.

Isn't this a bug and that particular change should therefore be backported? I recently got surprised by this behavior of raise when using BaseException.with_traceback in Python 3.10: I was able to set e.__traceback__ to a custom traceback with e.with_traceback(my_custom_tb), even verified with e.__traceback__ == my_custom_tb, yet a bare raise raised the exception like no changes had been made. (Also adding to this confusion: Changes to the original traceback like e.__traceback__.tb_next = None did successfully show up with bare raise.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I originally wanted to post this as an issue but discovered that the behavior seemingly disappeared when using Python 3.11. With some help I found out about this PR where one of the changes fixes the behavior of bare raise. So I thought it would be the best idea to ask here via comment since @iritkatriel is also listed as the maintainer for the traceback module anyway? I hope that's okay.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a short example to explain what I mean:

Python 3.10.11 (main, May  4 2023, 06:08:16) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def f(): g()
... 
>>> def g(): h()
... 
>>> def h(): raise Exception
... 
>>> # Trying to show only the "most recent" step of the traceback; doesn't work:
>>> try:
...     f()
... except Exception as e:
...     tb = e.__traceback__
...     while tb.tb_next is not None:
...         tb = tb.tb_next
...     e.with_traceback(tb)
...     raise
... 
Exception()
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 1, in f
  File "<stdin>", line 1, in g
  File "<stdin>", line 1, in h
Exception
>>> # However, modifying the traceback is possible in general. For example,
>>> # reducing the traceback to the "earliest" step of the original traceback:
>>> try:
...     f()
... except Exception as e:
...     e.__traceback__.tb_next = None
...     raise
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception
>>> 

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't backport this change, it's way too invasive for that. This behaviour existed since 3.0, unfortunately.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you "raise e" instead of "raise" then you will see the edited traceback (but with the current frame added).


.. _break:

The :keyword:`!break` statement
Expand Down
27 changes: 27 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ Other CPython Implementation Changes
hash-based pyc files now use ``siphash13``, too.
(Contributed by Inada Naoki in :issue:`29410`.)

* When an active exception is re-raised by a :keyword:`raise` statement with no parameters,
the traceback attached to this exception is now always ``sys.exc_info()[1].__traceback__``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of sys.exc_info()[1] always puts me off -- it's just too cryptic. Maybe "the traceback attached to this exception is now always its __traceback__ attribute? Or use the phrasing from the reference manual above?

This means that changes made to the traceback in the current :keyword:`except` clause are
reflected in the re-raised exception.
(Contributed by Irit Katriel in :issue:`45711`.)

New Modules
===========

Expand Down Expand Up @@ -266,6 +272,16 @@ sqlite3
(Contributed by Erlend E. Aasland in :issue:`45828`.)


sys
---

* :func:`sys.exc_info` now derives the ``type`` and ``traceback`` fields
from the ``value`` (the exception instance), so when an exception is
modified while it is being handled, the changes are reflected in
the results of subsequent calls to :func:`exc_info`.
(Contributed by Irit Katriel in :issue:`45711`.)


threading
---------

Expand Down Expand Up @@ -579,6 +595,17 @@ New Features
suspend and resume tracing and profiling.
(Contributed by Victor Stinner in :issue:`43760`.)

* :c:func:`PyErr_SetExcInfo()` no longer uses the ``type`` and ``traceback``
arguments, the interpreter now derives those values from the exception
instance (the ``value`` argument). The function still steals references
of all three arguments.
(Contributed by Irit Katriel in :issue:`45711`.)

* :c:func:`PyErr_GetExcInfo()` now derives the ``type`` and ``traceback``
fields of the result from the exception instance (the ``value`` field).
(Contributed by Irit Katriel in :issue:`45711`.)


Porting to Python 3.11
----------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The three values of ``exc_info`` are now always consistent with each other.
In particular, the ``type`` and ``traceback`` fields are now derived from
the exception instance. This impacts the return values of :func:`sys.exc_info`
and :c:func:`PyErr_GetExcInfo()` if the exception instance is modified while
the exception is handled, as well as :c:func:`PyErr_SetExcInfo()`, which now
ignores the ``type`` and ``traceback`` arguments provided to it.
9 changes: 3 additions & 6 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -5918,20 +5918,17 @@ do_raise(PyThreadState *tstate, PyObject *exc, PyObject *cause)
if (exc == NULL) {
/* Reraise */
_PyErr_StackItem *exc_info = _PyErr_GetTopmostException(tstate);
PyObject *tb;
type = exc_info->exc_type;
value = exc_info->exc_value;
tb = exc_info->exc_traceback;
assert(((Py_IsNone(value) || value == NULL)) ==
((Py_IsNone(type) || type == NULL)));
if (Py_IsNone(value) || value == NULL) {
_PyErr_SetString(tstate, PyExc_RuntimeError,
"No active exception to reraise");
return 0;
}
assert(PyExceptionInstance_Check(value));
type = PyExceptionInstance_Class(value);
Py_XINCREF(type);
Py_XINCREF(value);
Py_XINCREF(tb);
PyObject *tb = PyException_GetTraceback(value); /* new ref */
_PyErr_Restore(tstate, type, value, tb);
return 1;
}
Expand Down
74 changes: 48 additions & 26 deletions Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -470,25 +470,43 @@ PyErr_Clear(void)
_PyErr_Clear(tstate);
}

static PyObject*
get_exc_type(PyObject *exc_value) /* returns a borrowed ref */
{
if (exc_value == NULL || exc_value == Py_None) {
return Py_None;
}
else {
assert(PyExceptionInstance_Check(exc_value));
PyObject *type = PyExceptionInstance_Class(exc_value);
assert(type != NULL);
return type;
}
}

static PyObject*
get_exc_traceback(PyObject *exc_value) /* returns a borrowed ref */
{
if (exc_value == NULL || exc_value == Py_None) {
return Py_None;
}
else {
assert(PyExceptionInstance_Check(exc_value));
PyObject *tb = PyException_GetTraceback(exc_value);
Py_XDECREF(tb);
return tb ? tb : Py_None;
}
}

void
_PyErr_GetExcInfo(PyThreadState *tstate,
PyObject **p_type, PyObject **p_value, PyObject **p_traceback)
{
_PyErr_StackItem *exc_info = _PyErr_GetTopmostException(tstate);

*p_type = get_exc_type(exc_info->exc_value);
*p_value = exc_info->exc_value;
*p_traceback = exc_info->exc_traceback;

if (*p_value == NULL || *p_value == Py_None) {
assert(exc_info->exc_type == NULL || exc_info->exc_type == Py_None);
*p_type = Py_None;
}
else {
assert(PyExceptionInstance_Check(*p_value));
assert(exc_info->exc_type == PyExceptionInstance_Class(*p_value));
*p_type = PyExceptionInstance_Class(*p_value);
}
*p_traceback = get_exc_traceback(exc_info->exc_value);

Py_XINCREF(*p_type);
Py_XINCREF(*p_value);
Expand All @@ -504,7 +522,7 @@ PyErr_GetExcInfo(PyObject **p_type, PyObject **p_value, PyObject **p_traceback)
}

void
PyErr_SetExcInfo(PyObject *p_type, PyObject *p_value, PyObject *p_traceback)
PyErr_SetExcInfo(PyObject *type, PyObject *value, PyObject *traceback)
{
PyObject *oldtype, *oldvalue, *oldtraceback;
PyThreadState *tstate = _PyThreadState_GET();
Expand All @@ -513,9 +531,16 @@ PyErr_SetExcInfo(PyObject *p_type, PyObject *p_value, PyObject *p_traceback)
oldvalue = tstate->exc_info->exc_value;
oldtraceback = tstate->exc_info->exc_traceback;

tstate->exc_info->exc_type = p_type;
tstate->exc_info->exc_value = p_value;
tstate->exc_info->exc_traceback = p_traceback;

tstate->exc_info->exc_type = get_exc_type(value);
Py_XINCREF(tstate->exc_info->exc_type);
tstate->exc_info->exc_value = value;
tstate->exc_info->exc_traceback = get_exc_traceback(value);
Py_XINCREF(tstate->exc_info->exc_traceback);

/* These args are no longer used, but we still need to steal a ref */
Py_XDECREF(type);
Py_XDECREF(traceback);

Py_XDECREF(oldtype);
Py_XDECREF(oldvalue);
Expand All @@ -527,22 +552,19 @@ PyObject*
_PyErr_StackItemToExcInfoTuple(_PyErr_StackItem *err_info)
{
PyObject *exc_value = err_info->exc_value;
if (exc_value == NULL) {
exc_value = Py_None;
}

assert(exc_value == Py_None || PyExceptionInstance_Check(exc_value));
assert(exc_value == NULL ||
exc_value == Py_None ||
PyExceptionInstance_Check(exc_value));

PyObject *exc_type = PyExceptionInstance_Check(exc_value) ?
PyExceptionInstance_Class(exc_value) :
Py_None;
PyObject *exc_type = get_exc_type(exc_value);
PyObject *exc_traceback = get_exc_traceback(exc_value);

return Py_BuildValue(
"(OOO)",
exc_type,
exc_value,
err_info->exc_traceback != NULL ?
err_info->exc_traceback : Py_None);
exc_type ? exc_type : Py_None,
exc_value ? exc_value : Py_None,
exc_traceback ? exc_traceback : Py_None);
}


Expand Down