gh-102778: Add sys.last_exc, deprecate sys.last_type, sys.last_value,sys.last_traceback (#102779)

This commit is contained in:
Irit Katriel 2023-03-18 11:47:11 +00:00 committed by GitHub
parent 039714d00f
commit e1e9bab006
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 103 additions and 36 deletions

View file

@ -1102,22 +1102,25 @@ always available.
.. versionadded:: 3.5 .. versionadded:: 3.5
.. data:: last_exc
This variable is not always defined; it is set to the exception instance
when an exception is not handled and the interpreter prints an error message
and a stack traceback. Its intended use is to allow an interactive user to
import a debugger module and engage in post-mortem debugging without having
to re-execute the command that caused the error. (Typical use is
``import pdb; pdb.pm()`` to enter the post-mortem debugger; see :mod:`pdb`
module for more information.)
.. versionadded:: 3.12
.. data:: last_type .. data:: last_type
last_value last_value
last_traceback last_traceback
These three variables are not always defined; they are set when an exception is These three variables are deprecated; use :data:`sys.last_exc` instead.
not handled and the interpreter prints an error message and a stack traceback. They hold the legacy representation of ``sys.last_exc``, as returned
Their intended use is to allow an interactive user to import a debugger module from :func:`exc_info` above.
and engage in post-mortem debugging without having to re-execute the command
that caused the error. (Typical use is ``import pdb; pdb.pm()`` to enter the
post-mortem debugger; see :mod:`pdb` module for
more information.)
The meaning of the variables is the same as that of the return values from
:func:`exc_info` above.
.. data:: maxsize .. data:: maxsize

View file

@ -397,6 +397,12 @@ sys
with contributions from Gregory P. Smith [Google] and Mark Shannon with contributions from Gregory P. Smith [Google] and Mark Shannon
in :gh:`96123`.) in :gh:`96123`.)
* Add :data:`sys.last_exc` which holds the last unhandled exception that
was raised (for post-mortem debugging use cases). Deprecate the
three fields that have the same information in its legacy form:
:data:`sys.last_type`, :data:`sys.last_value` and :data:`sys.last_traceback`.
(Contributed by Irit Katriel in :gh:`102778`.)
Optimizations Optimizations
============= =============
@ -488,6 +494,10 @@ Deprecated
contain the creation time, which is also available in the new ``st_birthtime`` contain the creation time, which is also available in the new ``st_birthtime``
field. (Contributed by Steve Dower in :gh:`99726`.) field. (Contributed by Steve Dower in :gh:`99726`.)
* The :data:`sys.last_type`, :data:`sys.last_value` and :data:`sys.last_traceback`
fields are deprecated. Use :data:`sys.last_exc` instead.
(Contributed by Irit Katriel in :gh:`102778`.)
Pending Removal in Python 3.13 Pending Removal in Python 3.13
------------------------------ ------------------------------

View file

@ -995,6 +995,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kw2)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kw2));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(lambda)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(lambda));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_exc));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_node)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_node));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_traceback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_traceback));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_type)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_type));

View file

@ -481,6 +481,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(kw2) STRUCT_FOR_ID(kw2)
STRUCT_FOR_ID(lambda) STRUCT_FOR_ID(lambda)
STRUCT_FOR_ID(last) STRUCT_FOR_ID(last)
STRUCT_FOR_ID(last_exc)
STRUCT_FOR_ID(last_node) STRUCT_FOR_ID(last_node)
STRUCT_FOR_ID(last_traceback) STRUCT_FOR_ID(last_traceback)
STRUCT_FOR_ID(last_type) STRUCT_FOR_ID(last_type)

View file

@ -987,6 +987,7 @@ extern "C" {
INIT_ID(kw2), \ INIT_ID(kw2), \
INIT_ID(lambda), \ INIT_ID(lambda), \
INIT_ID(last), \ INIT_ID(last), \
INIT_ID(last_exc), \
INIT_ID(last_node), \ INIT_ID(last_node), \
INIT_ID(last_traceback), \ INIT_ID(last_traceback), \
INIT_ID(last_type), \ INIT_ID(last_type), \

View file

@ -1296,6 +1296,9 @@ _PyUnicode_InitStaticStrings(void) {
string = &_Py_ID(last); string = &_Py_ID(last);
assert(_PyUnicode_CheckConsistency(string, 1)); assert(_PyUnicode_CheckConsistency(string, 1));
PyUnicode_InternInPlace(&string); PyUnicode_InternInPlace(&string);
string = &_Py_ID(last_exc);
assert(_PyUnicode_CheckConsistency(string, 1));
PyUnicode_InternInPlace(&string);
string = &_Py_ID(last_node); string = &_Py_ID(last_node);
assert(_PyUnicode_CheckConsistency(string, 1)); assert(_PyUnicode_CheckConsistency(string, 1));
PyUnicode_InternInPlace(&string); PyUnicode_InternInPlace(&string);

View file

@ -106,6 +106,7 @@ def showsyntaxerror(self, filename=None):
""" """
type, value, tb = sys.exc_info() type, value, tb = sys.exc_info()
sys.last_exc = value
sys.last_type = type sys.last_type = type
sys.last_value = value sys.last_value = value
sys.last_traceback = tb sys.last_traceback = tb
@ -119,7 +120,7 @@ def showsyntaxerror(self, filename=None):
else: else:
# Stuff in the right filename # Stuff in the right filename
value = SyntaxError(msg, (filename, lineno, offset, line)) value = SyntaxError(msg, (filename, lineno, offset, line))
sys.last_value = value sys.last_exc = sys.last_value = value
if sys.excepthook is sys.__excepthook__: if sys.excepthook is sys.__excepthook__:
lines = traceback.format_exception_only(type, value) lines = traceback.format_exception_only(type, value)
self.write(''.join(lines)) self.write(''.join(lines))
@ -138,6 +139,7 @@ def showtraceback(self):
""" """
sys.last_type, sys.last_value, last_tb = ei = sys.exc_info() sys.last_type, sys.last_value, last_tb = ei = sys.exc_info()
sys.last_traceback = last_tb sys.last_traceback = last_tb
sys.last_exc = ei[1]
try: try:
lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next) lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next)
if sys.excepthook is sys.__excepthook__: if sys.excepthook is sys.__excepthook__:

View file

@ -118,7 +118,10 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False):
"""Disassemble a traceback (default: last traceback).""" """Disassemble a traceback (default: last traceback)."""
if tb is None: if tb is None:
try: try:
tb = sys.last_traceback if hasattr(sys, 'last_exc'):
tb = sys.last_exc.__traceback__
else:
tb = sys.last_traceback
except AttributeError: except AttributeError:
raise RuntimeError("no last traceback to disassemble") from None raise RuntimeError("no last traceback to disassemble") from None
while tb.tb_next: tb = tb.tb_next while tb.tb_next: tb = tb.tb_next

View file

@ -19,6 +19,7 @@ def setUpClass(cls):
except NameError: except NameError:
svs.last_type, svs.last_value, svs.last_traceback = ( svs.last_type, svs.last_value, svs.last_traceback = (
sys.exc_info()) sys.exc_info())
svs.last_exc = svs.last_value
requires('gui') requires('gui')
cls.root = Tk() cls.root = Tk()
@ -27,7 +28,7 @@ def setUpClass(cls):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
svs = stackviewer.sys svs = stackviewer.sys
del svs.last_traceback, svs.last_type, svs.last_value del svs.last_exc, svs.last_traceback, svs.last_type, svs.last_value
cls.root.update_idletasks() cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'): ## for id in cls.root.tk.call('after', 'info'):

View file

@ -1367,11 +1367,14 @@ def open_stack_viewer(self, event=None):
if self.interp.rpcclt: if self.interp.rpcclt:
return self.interp.remote_stack_viewer() return self.interp.remote_stack_viewer()
try: try:
sys.last_traceback if hasattr(sys, 'last_exc'):
sys.last_exc.__traceback__
else:
sys.last_traceback
except: except:
messagebox.showerror("No stack trace", messagebox.showerror("No stack trace",
"There is no stack trace yet.\n" "There is no stack trace yet.\n"
"(sys.last_traceback is not defined)", "(sys.last_exc and sys.last_traceback are not defined)",
parent=self.text) parent=self.text)
return return
from idlelib.stackviewer import StackBrowser from idlelib.stackviewer import StackBrowser

View file

@ -239,6 +239,7 @@ def print_exception():
efile = sys.stderr efile = sys.stderr
typ, val, tb = excinfo = sys.exc_info() typ, val, tb = excinfo = sys.exc_info()
sys.last_type, sys.last_value, sys.last_traceback = excinfo sys.last_type, sys.last_value, sys.last_traceback = excinfo
sys.last_exc = val
seen = set() seen = set()
def print_exc(typ, exc, tb): def print_exc(typ, exc, tb):
@ -629,6 +630,7 @@ def stackviewer(self, flist_oid=None):
flist = self.rpchandler.get_remote_proxy(flist_oid) flist = self.rpchandler.get_remote_proxy(flist_oid)
while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]: while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]:
tb = tb.tb_next tb = tb.tb_next
sys.last_exc = val
sys.last_type = typ sys.last_type = typ
sys.last_value = val sys.last_value = val
item = stackviewer.StackTreeItem(flist, tb) item = stackviewer.StackTreeItem(flist, tb)

View file

@ -27,7 +27,10 @@ def __init__(self, flist=None, tb=None):
def get_stack(self, tb): def get_stack(self, tb):
if tb is None: if tb is None:
tb = sys.last_traceback if hasattr(sys, 'last_exc'):
tb = sys.last_exc.__traceback__
else:
tb = sys.last_traceback
stack = [] stack = []
if tb and tb.tb_frame is None: if tb and tb.tb_frame is None:
tb = tb.tb_next tb = tb.tb_next
@ -37,11 +40,15 @@ def get_stack(self, tb):
return stack return stack
def get_exception(self): def get_exception(self):
type = sys.last_type if hasattr(sys, 'last_exc'):
value = sys.last_value typ = type(sys.last_exc)
if hasattr(type, "__name__"): value = sys.last_exc
type = type.__name__ else:
s = str(type) typ = sys.last_type
value = sys.last_value
if hasattr(typ, "__name__"):
typ = typ.__name__
s = str(typ)
if value is not None: if value is not None:
s = s + ": " + str(value) s = s + ": " + str(value)
return s return s
@ -136,6 +143,7 @@ def _stack_viewer(parent): # htest #
except NameError: except NameError:
exc_type, exc_value, exc_tb = sys.exc_info() exc_type, exc_value, exc_tb = sys.exc_info()
# inject stack trace to sys # inject stack trace to sys
sys.last_exc = exc_value
sys.last_type = exc_type sys.last_type = exc_type
sys.last_value = exc_value sys.last_value = exc_value
sys.last_traceback = exc_tb sys.last_traceback = exc_tb
@ -143,6 +151,7 @@ def _stack_viewer(parent): # htest #
StackBrowser(top, flist=flist, top=top, tb=exc_tb) StackBrowser(top, flist=flist, top=top, tb=exc_tb)
# restore sys to original state # restore sys to original state
del sys.last_exc
del sys.last_type del sys.last_type
del sys.last_value del sys.last_value
del sys.last_traceback del sys.last_traceback

View file

@ -1739,7 +1739,11 @@ def post_mortem(t=None):
def pm(): def pm():
"""Enter post-mortem debugging of the traceback found in sys.last_traceback.""" """Enter post-mortem debugging of the traceback found in sys.last_traceback."""
post_mortem(sys.last_traceback) if hasattr(sys, 'last_exc'):
tb = sys.last_exc.__traceback__
else:
tb = sys.last_traceback
post_mortem(tb)
# Main program for testing # Main program for testing

View file

@ -4799,7 +4799,7 @@
'pdb.pm()\n' 'pdb.pm()\n'
'\n' '\n'
' Enter post-mortem debugging of the traceback found in\n' ' Enter post-mortem debugging of the traceback found in\n'
' "sys.last_traceback".\n' ' "sys.last_exc".\n'
'\n' '\n'
'The "run*" functions and "set_trace()" are aliases for ' 'The "run*" functions and "set_trace()" are aliases for '
'instantiating\n' 'instantiating\n'
@ -13858,7 +13858,7 @@
'if\n' 'if\n'
' the interpreter is interactive, it is also made available to ' ' the interpreter is interactive, it is also made available to '
'the\n' 'the\n'
' user as "sys.last_traceback".\n' ' user as "sys.last_exc".\n'
'\n' '\n'
' For explicitly created tracebacks, it is up to the creator ' ' For explicitly created tracebacks, it is up to the creator '
'of\n' 'of\n'

View file

@ -1026,6 +1026,10 @@ def test_disassemble_try_finally(self):
self.do_disassembly_test(_tryfinallyconst, dis_tryfinallyconst) self.do_disassembly_test(_tryfinallyconst, dis_tryfinallyconst)
def test_dis_none(self): def test_dis_none(self):
try:
del sys.last_exc
except AttributeError:
pass
try: try:
del sys.last_traceback del sys.last_traceback
except AttributeError: except AttributeError:
@ -1043,7 +1047,7 @@ def test_dis_traceback(self):
1/0 1/0
except Exception as e: except Exception as e:
tb = e.__traceback__ tb = e.__traceback__
sys.last_traceback = tb sys.last_exc = e
tb_dis = self.get_disassemble_as_string(tb.tb_frame.f_code, tb.tb_lasti) tb_dis = self.get_disassemble_as_string(tb.tb_frame.f_code, tb.tb_lasti)
self.do_disassembly_test(None, tb_dis, True) self.do_disassembly_test(None, tb_dis, True)
@ -1900,6 +1904,10 @@ def test_findlabels(self):
class TestDisTraceback(DisTestBase): class TestDisTraceback(DisTestBase):
def setUp(self) -> None: def setUp(self) -> None:
try: # We need to clean up existing tracebacks
del sys.last_exc
except AttributeError:
pass
try: # We need to clean up existing tracebacks try: # We need to clean up existing tracebacks
del sys.last_traceback del sys.last_traceback
except AttributeError: except AttributeError:

View file

@ -45,7 +45,9 @@ def test_widget_destroy(self):
# value which causes the tracing callback to be called and then # value which causes the tracing callback to be called and then
# it tries calling instance attributes not yet defined. # it tries calling instance attributes not yet defined.
ttk.LabeledScale(self.root, variable=myvar) ttk.LabeledScale(self.root, variable=myvar)
if hasattr(sys, 'last_type'): if hasattr(sys, 'last_exc'):
self.assertNotEqual(type(sys.last_exc), tkinter.TclError)
elif hasattr(sys, 'last_type'):
self.assertNotEqual(sys.last_type, tkinter.TclError) self.assertNotEqual(sys.last_type, tkinter.TclError)
def test_initialization(self): def test_initialization(self):

View file

@ -2400,6 +2400,7 @@ def report_callback_exception(self, exc, val, tb):
should when sys.stderr is None.""" should when sys.stderr is None."""
import traceback import traceback
print("Exception in Tkinter callback", file=sys.stderr) print("Exception in Tkinter callback", file=sys.stderr)
sys.last_exc = val
sys.last_type = exc sys.last_type = exc
sys.last_value = val sys.last_value = val
sys.last_traceback = tb sys.last_traceback = tb

View file

@ -179,7 +179,7 @@ def _safe_string(value, what, func=str):
# -- # --
def print_exc(limit=None, file=None, chain=True): def print_exc(limit=None, file=None, chain=True):
"""Shorthand for 'print_exception(*sys.exc_info(), limit, file)'.""" """Shorthand for 'print_exception(*sys.exc_info(), limit, file, chain)'."""
print_exception(*sys.exc_info(), limit=limit, file=file, chain=chain) print_exception(*sys.exc_info(), limit=limit, file=file, chain=chain)
def format_exc(limit=None, chain=True): def format_exc(limit=None, chain=True):
@ -187,12 +187,16 @@ def format_exc(limit=None, chain=True):
return "".join(format_exception(*sys.exc_info(), limit=limit, chain=chain)) return "".join(format_exception(*sys.exc_info(), limit=limit, chain=chain))
def print_last(limit=None, file=None, chain=True): def print_last(limit=None, file=None, chain=True):
"""This is a shorthand for 'print_exception(sys.last_type, """This is a shorthand for 'print_exception(sys.last_exc, limit, file, chain)'."""
sys.last_value, sys.last_traceback, limit, file)'.""" if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"):
if not hasattr(sys, "last_type"):
raise ValueError("no last exception") raise ValueError("no last exception")
print_exception(sys.last_type, sys.last_value, sys.last_traceback,
limit, file, chain) if hasattr(sys, "last_exc"):
print_exception(sys.last_exc, limit, file, chain)
else:
print_exception(sys.last_type, sys.last_value, sys.last_traceback,
limit, file, chain)
# #
# Printing and Extracting Stacks. # Printing and Extracting Stacks.

View file

@ -0,0 +1,3 @@
Add :data:`sys.last_exc` and deprecate :data:`sys.last_type`, :data:`sys.last_value`
and :data:`sys.last_traceback`,
which hold the same information in its legacy form.

View file

@ -1304,7 +1304,7 @@ finalize_modules_delete_special(PyThreadState *tstate, int verbose)
{ {
// List of names to clear in sys // List of names to clear in sys
static const char * const sys_deletes[] = { static const char * const sys_deletes[] = {
"path", "argv", "ps1", "ps2", "path", "argv", "ps1", "ps2", "last_exc",
"last_type", "last_value", "last_traceback", "last_type", "last_value", "last_traceback",
"__interactivehook__", "__interactivehook__",
// path_hooks and path_importer_cache are cleared // path_hooks and path_importer_cache are cleared

View file

@ -776,6 +776,10 @@ _PyErr_PrintEx(PyThreadState *tstate, int set_sys_last_vars)
} }
if (set_sys_last_vars) { if (set_sys_last_vars) {
if (_PySys_SetAttr(&_Py_ID(last_exc), exc) < 0) {
_PyErr_Clear(tstate);
}
/* Legacy version: */
if (_PySys_SetAttr(&_Py_ID(last_type), typ) < 0) { if (_PySys_SetAttr(&_Py_ID(last_type), typ) < 0) {
_PyErr_Clear(tstate); _PyErr_Clear(tstate);
} }

View file

@ -2670,11 +2670,13 @@ stderr -- standard error object; used for error messages\n\
By assigning other file objects (or objects that behave like files)\n\ By assigning other file objects (or objects that behave like files)\n\
to these, it is possible to redirect all of the interpreter's I/O.\n\ to these, it is possible to redirect all of the interpreter's I/O.\n\
\n\ \n\
last_exc - the last uncaught exception\n\
Only available in an interactive session after a\n\
traceback has been printed.\n\
last_type -- type of last uncaught exception\n\ last_type -- type of last uncaught exception\n\
last_value -- value of last uncaught exception\n\ last_value -- value of last uncaught exception\n\
last_traceback -- traceback of last uncaught exception\n\ last_traceback -- traceback of last uncaught exception\n\
These three are only available in an interactive session after a\n\ These three are the (deprecated) legacy representation of last_exc.\n\
traceback has been printed.\n\
" "
) )
/* concatenating string here */ /* concatenating string here */