From aad78281d27df18c26eb923c9d6a3b80027fbc18 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 23 Nov 2023 09:58:38 +0000 Subject: [PATCH 1/5] gh-112332: save snapshot of exc_type in TracebackException. Add option to not save the exc_type itself. --- Lib/traceback.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index b25a7291f6be51..a0d7d5f0dee7c7 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -737,7 +737,7 @@ class TracebackException: def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False, - max_group_width=15, max_group_depth=10, _seen=None): + max_group_width=15, max_group_depth=10, save_exc_type=True, _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we # need stub thunk objects just to glue it together. @@ -754,12 +754,23 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, _walk_tb_with_full_positions(exc_traceback), limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals) - self.exc_type = exc_type + + self.exc_type = exc_type if save_exc_type else None + # Capture now to permit freeing resources: only complication is in the # unofficial API _format_final_exc_line self._str = _safe_string(exc_value, 'exception') self.__notes__ = getattr(exc_value, '__notes__', None) + self._is_syntax_error = False + self._have_exc_type = exc_type is not None + if exc_type is not None: + self._exc_type_qualname = exc_type.__qualname__ + self._exc_type_module = exc_type.__module__ + else: + self._exc_type_qualname = None + self._exc_type_module = None + if exc_type and issubclass(exc_type, SyntaxError): # Handle SyntaxError's specially self.filename = exc_value.filename @@ -771,6 +782,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self.offset = exc_value.offset self.end_offset = exc_value.end_offset self.msg = exc_value.msg + self._is_syntax_error = True elif exc_type and issubclass(exc_type, ImportError) and \ getattr(exc_value, "name_from", None) is not None: wrong_name = getattr(exc_value, "name_from", None) @@ -901,18 +913,18 @@ def format_exception_only(self, *, show_group=False, _depth=0): """ indent = 3 * _depth * ' ' - if self.exc_type is None: + if not self._have_exc_type: yield indent + _format_final_exc_line(None, self._str) return - stype = self.exc_type.__qualname__ - smod = self.exc_type.__module__ + stype = self._exc_type_qualname + smod = self._exc_type_module if smod not in ("__main__", "builtins"): if not isinstance(smod, str): smod = "" stype = smod + '.' + stype - if not issubclass(self.exc_type, SyntaxError): + if not self._is_syntax_error: if _depth > 0: # Nested exceptions needs correct handling of multiline messages. formatted = _format_final_exc_line( From 5aea01a41b06a5a3b9dd5e1b3d2ee1cf2c57e984 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 23 Nov 2023 10:21:27 +0000 Subject: [PATCH 2/5] add depracation and doc --- Doc/library/traceback.rst | 8 ++++++++ Doc/whatsnew/3.13.rst | 11 +++++++++++ Lib/traceback.py | 40 ++++++++++++++++++++++++++------------- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 408da7fc5f0645..80dda5ec520d7a 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -287,6 +287,14 @@ capture data for later printing in a lightweight fashion. The class of the original traceback. + .. deprecated:: 3.13 + + .. attribute:: exc_type_str + + String display of the class of the original exception. + + .. versionadded:: 3.13 + .. attribute:: filename For syntax errors - the file name where the error occurred. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 3fd0f5e165f018..389017448c9d8e 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -308,6 +308,12 @@ traceback to format the nested exceptions of a :exc:`BaseExceptionGroup` instance, recursively. (Contributed by Irit Katriel in :gh:`105292`.) +* Add the field *exc_type_str* to :class:`~traceback.TracebackException`, which + holds a string display of the *exc_type*. Deprecate the field *exc_type* + which holds the type object itself. Add parameter *save_exc_type* (default + ``True``) to indicate whether ``exc_type`` should be saved. + (Contributed by Irit Katriel in :gh:`112332`.) + typing ------ @@ -367,6 +373,11 @@ Deprecated security and functionality bugs. This includes removal of the ``--cgi`` flag to the ``python -m http.server`` command line in 3.15. +* :mod:`traceback`: + + * The field *exc_type* of :class:`traceback.TracebackException` is + deprecated. Use *exc_type_str* instead. + * :mod:`typing`: * Creating a :class:`typing.NamedTuple` class using keyword arguments to denote diff --git a/Lib/traceback.py b/Lib/traceback.py index a0d7d5f0dee7c7..5d83f85ac3edb0 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -5,6 +5,7 @@ import linecache import sys import textwrap +import warnings from contextlib import suppress __all__ = ['extract_stack', 'extract_tb', 'format_exception', @@ -719,7 +720,8 @@ class TracebackException: - :attr:`__suppress_context__` The *__suppress_context__* value from the original exception. - :attr:`stack` A `StackSummary` representing the traceback. - - :attr:`exc_type` The class of the original traceback. + - :attr:`exc_type` (deprecated) The class of the original traceback. + - :attr:`exc_type_str` String display of exc_type - :attr:`filename` For syntax errors - the filename where the error occurred. - :attr:`lineno` For syntax errors - the linenumber where the error @@ -755,7 +757,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals) - self.exc_type = exc_type if save_exc_type else None + self._exc_type = exc_type if save_exc_type else None # Capture now to permit freeing resources: only complication is in the # unofficial API _format_final_exc_line @@ -765,11 +767,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self._is_syntax_error = False self._have_exc_type = exc_type is not None if exc_type is not None: - self._exc_type_qualname = exc_type.__qualname__ - self._exc_type_module = exc_type.__module__ + self.exc_type_qualname = exc_type.__qualname__ + self.exc_type_module = exc_type.__module__ else: - self._exc_type_qualname = None - self._exc_type_module = None + self.exc_type_qualname = None + self.exc_type_module = None if exc_type and issubclass(exc_type, SyntaxError): # Handle SyntaxError's specially @@ -881,6 +883,24 @@ def from_exception(cls, exc, *args, **kwargs): """Create a TracebackException from an exception.""" return cls(type(exc), exc, exc.__traceback__, *args, **kwargs) + @property + def exc_type(self): + warnings.warn('Deprecated in 3.13. Use exc_type_str instead.', + DeprecationWarning, stacklevel=2) + return self._exc_type + + @property + def exc_type_str(self): + if not self._have_exc_type: + return None + stype = self.exc_type_qualname + smod = self.exc_type_module + if smod not in ("__main__", "builtins"): + if not isinstance(smod, str): + smod = "" + stype = smod + '.' + stype + return stype + def _load_lines(self): """Private API. force all lines in the stack to be loaded.""" for frame in self.stack: @@ -917,13 +937,7 @@ def format_exception_only(self, *, show_group=False, _depth=0): yield indent + _format_final_exc_line(None, self._str) return - stype = self._exc_type_qualname - smod = self._exc_type_module - if smod not in ("__main__", "builtins"): - if not isinstance(smod, str): - smod = "" - stype = smod + '.' + stype - + stype = self.exc_type_str if not self._is_syntax_error: if _depth > 0: # Nested exceptions needs correct handling of multiline messages. From 59fe1637739e59d0d187c5355396ef3af201c5bd Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 23 Nov 2023 10:35:39 +0000 Subject: [PATCH 3/5] update tests --- Lib/test/test_traceback.py | 40 ++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index b43dca6f640b9a..95ed6cca3353fe 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -2715,9 +2715,9 @@ def __repr__(self) -> str: class TestTracebackException(unittest.TestCase): - def test_smoke(self): + def do_test_smoke(self, exc, expected_type_str): try: - 1/0 + raise exc except Exception as e: exc_obj = e exc = traceback.TracebackException.from_exception(e) @@ -2727,9 +2727,23 @@ def test_smoke(self): self.assertEqual(None, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) - self.assertEqual(type(exc_obj), exc.exc_type) + with self.assertWarns(DeprecationWarning): + self.assertEqual(type(exc_obj), exc.exc_type) + self.assertEqual(expected_type_str, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) + def test_smoke_builtin(self): + self.do_test_smoke(ValueError(42), 'ValueError') + + def test_smoke_user_exception(self): + class MyException(Exception): + pass + + self.do_test_smoke( + MyException('bad things happened'), + ('test.test_traceback.TestTracebackException.' + 'test_smoke_user_exception..MyException')) + def test_from_exception(self): # Check all the parameters are accepted. def foo(): @@ -2750,7 +2764,9 @@ def foo(): self.assertEqual(None, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) - self.assertEqual(type(exc_obj), exc.exc_type) + with self.assertWarns(DeprecationWarning): + self.assertEqual(type(exc_obj), exc.exc_type) + self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) def test_cause(self): @@ -2772,7 +2788,9 @@ def test_cause(self): self.assertEqual(exc_context, exc.__context__) self.assertEqual(True, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) - self.assertEqual(type(exc_obj), exc.exc_type) + with self.assertWarns(DeprecationWarning): + self.assertEqual(type(exc_obj), exc.exc_type) + self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) def test_context(self): @@ -2792,7 +2810,9 @@ def test_context(self): self.assertEqual(exc_context, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) - self.assertEqual(type(exc_obj), exc.exc_type) + with self.assertWarns(DeprecationWarning): + self.assertEqual(type(exc_obj), exc.exc_type) + self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) def test_long_context_chain(self): @@ -2837,7 +2857,9 @@ def test_compact_with_cause(self): self.assertEqual(None, exc.__context__) self.assertEqual(True, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) - self.assertEqual(type(exc_obj), exc.exc_type) + with self.assertWarns(DeprecationWarning): + self.assertEqual(type(exc_obj), exc.exc_type) + self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) def test_compact_no_cause(self): @@ -2857,7 +2879,9 @@ def test_compact_no_cause(self): self.assertEqual(exc_context, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) - self.assertEqual(type(exc_obj), exc.exc_type) + with self.assertWarns(DeprecationWarning): + self.assertEqual(type(exc_obj), exc.exc_type) + self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) def test_no_refs_to_exception_and_traceback_objects(self): From a2782447b697c11414c214300f3d2639c8573bcd Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 23 Nov 2023 10:41:26 +0000 Subject: [PATCH 4/5] Add news --- .../next/Library/2023-11-23-10-41-21.gh-issue-112332.rhTBaa.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-11-23-10-41-21.gh-issue-112332.rhTBaa.rst diff --git a/Misc/NEWS.d/next/Library/2023-11-23-10-41-21.gh-issue-112332.rhTBaa.rst b/Misc/NEWS.d/next/Library/2023-11-23-10-41-21.gh-issue-112332.rhTBaa.rst new file mode 100644 index 00000000000000..bd686ad052e5b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-23-10-41-21.gh-issue-112332.rhTBaa.rst @@ -0,0 +1,2 @@ +Deprecate the ``exc_type`` field of :class:`traceback.TracebackException`. +Add ``exc_type_str`` to replace it. From 65615963ed0cb1cf499a3c8a94ba5f9032c54b34 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Thu, 23 Nov 2023 13:09:24 +0000 Subject: [PATCH 5/5] add test for save_exc_type=False --- Lib/test/test_traceback.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 95ed6cca3353fe..c58d979bdd0115 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -2884,6 +2884,17 @@ def test_compact_no_cause(self): self.assertEqual(type(exc_obj).__name__, exc.exc_type_str) self.assertEqual(str(exc_obj), str(exc)) + def test_no_save_exc_type(self): + try: + 1/0 + except Exception as e: + exc = e + + te = traceback.TracebackException.from_exception( + exc, save_exc_type=False) + with self.assertWarns(DeprecationWarning): + self.assertIsNone(te.exc_type) + def test_no_refs_to_exception_and_traceback_objects(self): try: 1/0