]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-112332: Deprecate TracebackException.exc_type, add exc_type_str. (#112333)
authorIrit Katriel <1055913+iritkatriel@users.noreply.github.com>
Tue, 28 Nov 2023 08:03:25 +0000 (08:03 +0000)
committerGitHub <noreply@github.com>
Tue, 28 Nov 2023 08:03:25 +0000 (08:03 +0000)
Doc/library/traceback.rst
Doc/whatsnew/3.13.rst
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS.d/next/Library/2023-11-23-10-41-21.gh-issue-112332.rhTBaa.rst [new file with mode: 0644]

index 408da7fc5f0645dad4d32c460218f8f8999b80b9..80dda5ec520d7a91a28f61ce9c6889f6369b8698 100644 (file)
@@ -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.
index bf6a70f2009b8e1598ed165961b0ff0e2726ac70..198ea3a4b57bde1e0430ed17b3f8b93b8ebdd95e 100644 (file)
@@ -318,6 +318,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
 ------
 
@@ -377,6 +383,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
index b43dca6f640b9ae4b823496eb25ed712de15742a..c58d979bdd01153e9faa74fa63746ac8a122fa46 100644 (file)
@@ -2715,9 +2715,9 @@ class Unrepresentable:
 
 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 @@ class TestTracebackException(unittest.TestCase):
         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.<locals>.MyException'))
+
     def test_from_exception(self):
         # Check all the parameters are accepted.
         def foo():
@@ -2750,7 +2764,9 @@ class TestTracebackException(unittest.TestCase):
         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 @@ class TestTracebackException(unittest.TestCase):
         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 @@ class TestTracebackException(unittest.TestCase):
         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 @@ class TestTracebackException(unittest.TestCase):
         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,9 +2879,22 @@ class TestTracebackException(unittest.TestCase):
         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_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
index b25a7291f6be510f03c77764c6cc9fa89597af85..5d83f85ac3edb0a61ae6dc5d75796503624aa499 100644 (file)
@@ -5,6 +5,7 @@ import itertools
 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
@@ -737,7 +739,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 +756,23 @@ class TracebackException:
             _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 +784,7 @@ class TracebackException:
             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)
@@ -869,6 +883,24 @@ class TracebackException:
         """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 = "<unknown>"
+            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:
@@ -901,18 +933,12 @@ class TracebackException:
         """
 
         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__
-        if smod not in ("__main__", "builtins"):
-            if not isinstance(smod, str):
-                smod = "<unknown>"
-            stype = smod + '.' + stype
-
-        if not issubclass(self.exc_type, SyntaxError):
+        stype = self.exc_type_str
+        if not self._is_syntax_error:
             if _depth > 0:
                 # Nested exceptions needs correct handling of multiline messages.
                 formatted = _format_final_exc_line(
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 (file)
index 0000000..bd686ad
--- /dev/null
@@ -0,0 +1,2 @@
+Deprecate the ``exc_type`` field of :class:`traceback.TracebackException`.
+Add ``exc_type_str`` to replace it.