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

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

Merged
merged 9 commits into from
Nov 30, 2021
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
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
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
6 changes: 6 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
9 changes: 3 additions & 6 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -5920,20 +5920,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