From e1e9bab0061e8d4bd7b94ed455f3bb7bf8633ae7 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Sat, 18 Mar 2023 11:47:11 +0000 Subject: gh-102778: Add sys.last_exc, deprecate sys.last_type, sys.last_value,sys.last_traceback (#102779) --- Doc/library/sys.rst | 25 ++++++++++++---------- Doc/whatsnew/3.12.rst | 10 +++++++++ .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + Include/internal/pycore_runtime_init_generated.h | 1 + Include/internal/pycore_unicodeobject_generated.h | 3 +++ Lib/code.py | 4 +++- Lib/dis.py | 5 ++++- Lib/idlelib/idle_test/test_stackviewer.py | 3 ++- Lib/idlelib/pyshell.py | 7 ++++-- Lib/idlelib/run.py | 2 ++ Lib/idlelib/stackviewer.py | 21 ++++++++++++------ Lib/pdb.py | 6 +++++- Lib/pydoc_data/topics.py | 4 ++-- Lib/test/test_dis.py | 10 ++++++++- Lib/test/test_ttk/test_extensions.py | 4 +++- Lib/tkinter/__init__.py | 1 + Lib/traceback.py | 16 ++++++++------ .../2023-03-17-13-43-34.gh-issue-102778.ANDv8I.rst | 3 +++ Python/pylifecycle.c | 2 +- Python/pythonrun.c | 4 ++++ Python/sysmodule.c | 6 ++++-- 22 files changed, 103 insertions(+), 36 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-03-17-13-43-34.gh-issue-102778.ANDv8I.rst diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index a53d490..b3b9b5e 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1102,22 +1102,25 @@ always available. .. 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 last_value last_traceback - These three variables are not always defined; they are set when an exception is - not handled and the interpreter prints an error message and a stack traceback. - Their 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.) - - The meaning of the variables is the same as that of the return values from - :func:`exc_info` above. - + These three variables are deprecated; use :data:`sys.last_exc` instead. + They hold the legacy representation of ``sys.last_exc``, as returned + from :func:`exc_info` above. .. data:: maxsize diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index b55b961..32fec96 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -397,6 +397,12 @@ sys with contributions from Gregory P. Smith [Google] and Mark Shannon 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 ============= @@ -488,6 +494,10 @@ Deprecated contain the creation time, which is also available in the new ``st_birthtime`` 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 ------------------------------ diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 4b12ae5..14dfd9e 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -995,6 +995,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kw2)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(lambda)); _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_traceback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_type)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 17fb9ff..6f430bb 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -481,6 +481,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(kw2) STRUCT_FOR_ID(lambda) STRUCT_FOR_ID(last) + STRUCT_FOR_ID(last_exc) STRUCT_FOR_ID(last_node) STRUCT_FOR_ID(last_traceback) STRUCT_FOR_ID(last_type) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index b240be5..0452c4c 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -987,6 +987,7 @@ extern "C" { INIT_ID(kw2), \ INIT_ID(lambda), \ INIT_ID(last), \ + INIT_ID(last_exc), \ INIT_ID(last_node), \ INIT_ID(last_traceback), \ INIT_ID(last_type), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index fea9b6d..0a88659 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1296,6 +1296,9 @@ _PyUnicode_InitStaticStrings(void) { string = &_Py_ID(last); assert(_PyUnicode_CheckConsistency(string, 1)); PyUnicode_InternInPlace(&string); + string = &_Py_ID(last_exc); + assert(_PyUnicode_CheckConsistency(string, 1)); + PyUnicode_InternInPlace(&string); string = &_Py_ID(last_node); assert(_PyUnicode_CheckConsistency(string, 1)); PyUnicode_InternInPlace(&string); diff --git a/Lib/code.py b/Lib/code.py index 76000f8..2bd5fa3 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -106,6 +106,7 @@ class InteractiveInterpreter: """ type, value, tb = sys.exc_info() + sys.last_exc = value sys.last_type = type sys.last_value = value sys.last_traceback = tb @@ -119,7 +120,7 @@ class InteractiveInterpreter: else: # Stuff in the right filename value = SyntaxError(msg, (filename, lineno, offset, line)) - sys.last_value = value + sys.last_exc = sys.last_value = value if sys.excepthook is sys.__excepthook__: lines = traceback.format_exception_only(type, value) self.write(''.join(lines)) @@ -138,6 +139,7 @@ class InteractiveInterpreter: """ sys.last_type, sys.last_value, last_tb = ei = sys.exc_info() sys.last_traceback = last_tb + sys.last_exc = ei[1] try: lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next) if sys.excepthook is sys.__excepthook__: diff --git a/Lib/dis.py b/Lib/dis.py index 9edde6a..c3d152b 100644 --- a/Lib/dis.py +++ b/Lib/dis.py @@ -118,7 +118,10 @@ def distb(tb=None, *, file=None, show_caches=False, adaptive=False): """Disassemble a traceback (default: last traceback).""" if tb is None: try: - tb = sys.last_traceback + if hasattr(sys, 'last_exc'): + tb = sys.last_exc.__traceback__ + else: + tb = sys.last_traceback except AttributeError: raise RuntimeError("no last traceback to disassemble") from None while tb.tb_next: tb = tb.tb_next diff --git a/Lib/idlelib/idle_test/test_stackviewer.py b/Lib/idlelib/idle_test/test_stackviewer.py index 98f53f9..f4626bb 100644 --- a/Lib/idlelib/idle_test/test_stackviewer.py +++ b/Lib/idlelib/idle_test/test_stackviewer.py @@ -19,6 +19,7 @@ class StackBrowserTest(unittest.TestCase): except NameError: svs.last_type, svs.last_value, svs.last_traceback = ( sys.exc_info()) + svs.last_exc = svs.last_value requires('gui') cls.root = Tk() @@ -27,7 +28,7 @@ class StackBrowserTest(unittest.TestCase): @classmethod def tearDownClass(cls): 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() ## for id in cls.root.tk.call('after', 'info'): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index e68233a..edc77ff 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1367,11 +1367,14 @@ class PyShell(OutputWindow): if self.interp.rpcclt: return self.interp.remote_stack_viewer() try: - sys.last_traceback + if hasattr(sys, 'last_exc'): + sys.last_exc.__traceback__ + else: + sys.last_traceback except: messagebox.showerror("No stack trace", "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) return from idlelib.stackviewer import StackBrowser diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 577c49e..6a7b50c 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -239,6 +239,7 @@ def print_exception(): efile = sys.stderr typ, val, tb = excinfo = sys.exc_info() sys.last_type, sys.last_value, sys.last_traceback = excinfo + sys.last_exc = val seen = set() def print_exc(typ, exc, tb): @@ -629,6 +630,7 @@ class Executive: flist = self.rpchandler.get_remote_proxy(flist_oid) while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]: tb = tb.tb_next + sys.last_exc = val sys.last_type = typ sys.last_value = val item = stackviewer.StackTreeItem(flist, tb) diff --git a/Lib/idlelib/stackviewer.py b/Lib/idlelib/stackviewer.py index 94ffb4e..702fd32 100644 --- a/Lib/idlelib/stackviewer.py +++ b/Lib/idlelib/stackviewer.py @@ -27,7 +27,10 @@ class StackTreeItem(TreeItem): def get_stack(self, tb): if tb is None: - tb = sys.last_traceback + if hasattr(sys, 'last_exc'): + tb = sys.last_exc.__traceback__ + else: + tb = sys.last_traceback stack = [] if tb and tb.tb_frame is None: tb = tb.tb_next @@ -37,11 +40,15 @@ class StackTreeItem(TreeItem): return stack def get_exception(self): - type = sys.last_type - value = sys.last_value - if hasattr(type, "__name__"): - type = type.__name__ - s = str(type) + if hasattr(sys, 'last_exc'): + typ = type(sys.last_exc) + value = sys.last_exc + else: + typ = sys.last_type + value = sys.last_value + if hasattr(typ, "__name__"): + typ = typ.__name__ + s = str(typ) if value is not None: s = s + ": " + str(value) return s @@ -136,6 +143,7 @@ def _stack_viewer(parent): # htest # except NameError: exc_type, exc_value, exc_tb = sys.exc_info() # inject stack trace to sys + sys.last_exc = exc_value sys.last_type = exc_type sys.last_value = exc_value sys.last_traceback = exc_tb @@ -143,6 +151,7 @@ def _stack_viewer(parent): # htest # StackBrowser(top, flist=flist, top=top, tb=exc_tb) # restore sys to original state + del sys.last_exc del sys.last_type del sys.last_value del sys.last_traceback diff --git a/Lib/pdb.py b/Lib/pdb.py index f11fc55..3543f53 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1739,7 +1739,11 @@ def post_mortem(t=None): def pm(): """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 diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 573065b..ad1b6ac 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -4799,7 +4799,7 @@ topics = {'assert': 'The "assert" statement\n' 'pdb.pm()\n' '\n' ' Enter post-mortem debugging of the traceback found in\n' - ' "sys.last_traceback".\n' + ' "sys.last_exc".\n' '\n' 'The "run*" functions and "set_trace()" are aliases for ' 'instantiating\n' @@ -13858,7 +13858,7 @@ topics = {'assert': 'The "assert" statement\n' 'if\n' ' the interpreter is interactive, it is also made available to ' 'the\n' - ' user as "sys.last_traceback".\n' + ' user as "sys.last_exc".\n' '\n' ' For explicitly created tracebacks, it is up to the creator ' 'of\n' diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index b77e3b0..fa1de1c 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -1027,6 +1027,10 @@ class DisTests(DisTestBase): def test_dis_none(self): try: + del sys.last_exc + except AttributeError: + pass + try: del sys.last_traceback except AttributeError: pass @@ -1043,7 +1047,7 @@ class DisTests(DisTestBase): 1/0 except Exception as e: 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) self.do_disassembly_test(None, tb_dis, True) @@ -1901,6 +1905,10 @@ class TestFinderMethods(unittest.TestCase): class TestDisTraceback(DisTestBase): 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 del sys.last_traceback except AttributeError: pass diff --git a/Lib/test/test_ttk/test_extensions.py b/Lib/test/test_ttk/test_extensions.py index 6135c49..d5e0697 100644 --- a/Lib/test/test_ttk/test_extensions.py +++ b/Lib/test/test_ttk/test_extensions.py @@ -45,7 +45,9 @@ class LabeledScaleTest(AbstractTkTest, unittest.TestCase): # value which causes the tracing callback to be called and then # it tries calling instance attributes not yet defined. 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) def test_initialization(self): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 7565e0f..479daf0 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -2400,6 +2400,7 @@ class Tk(Misc, Wm): should when sys.stderr is None.""" import traceback print("Exception in Tkinter callback", file=sys.stderr) + sys.last_exc = val sys.last_type = exc sys.last_value = val sys.last_traceback = tb diff --git a/Lib/traceback.py b/Lib/traceback.py index c43c472..9e720ac 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -179,7 +179,7 @@ def _safe_string(value, what, func=str): # -- 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) 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)) def print_last(limit=None, file=None, chain=True): - """This is a shorthand for 'print_exception(sys.last_type, - sys.last_value, sys.last_traceback, limit, file)'.""" - if not hasattr(sys, "last_type"): + """This is a shorthand for 'print_exception(sys.last_exc, limit, file, chain)'.""" + if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"): 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. diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-03-17-13-43-34.gh-issue-102778.ANDv8I.rst b/Misc/NEWS.d/next/Core and Builtins/2023-03-17-13-43-34.gh-issue-102778.ANDv8I.rst new file mode 100644 index 0000000..b5da227 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-03-17-13-43-34.gh-issue-102778.ANDv8I.rst @@ -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. diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index d0c65cc..7bf1227 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -1304,7 +1304,7 @@ finalize_modules_delete_special(PyThreadState *tstate, int verbose) { // List of names to clear in sys static const char * const sys_deletes[] = { - "path", "argv", "ps1", "ps2", + "path", "argv", "ps1", "ps2", "last_exc", "last_type", "last_value", "last_traceback", "__interactivehook__", // path_hooks and path_importer_cache are cleared diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 07d119a..6ea185a 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -776,6 +776,10 @@ _PyErr_PrintEx(PyThreadState *tstate, int 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) { _PyErr_Clear(tstate); } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index cc5b9a6..126b7d4 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -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\ to these, it is possible to redirect all of the interpreter's I/O.\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_value -- value 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\ - traceback has been printed.\n\ + These three are the (deprecated) legacy representation of last_exc.\n\ " ) /* concatenating string here */ -- cgit v0.12