Skip to content

Commit 2c68011

Browse files
authored
gh-112332: Deprecate TracebackException.exc_type, add exc_type_str. (#112333)
1 parent 2df26d8 commit 2c68011

File tree

5 files changed

+102
-20
lines changed

5 files changed

+102
-20
lines changed

Doc/library/traceback.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@ capture data for later printing in a lightweight fashion.
287287

288288
The class of the original traceback.
289289

290+
.. deprecated:: 3.13
291+
292+
.. attribute:: exc_type_str
293+
294+
String display of the class of the original exception.
295+
296+
.. versionadded:: 3.13
297+
290298
.. attribute:: filename
291299

292300
For syntax errors - the file name where the error occurred.

Doc/whatsnew/3.13.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ traceback
318318
to format the nested exceptions of a :exc:`BaseExceptionGroup` instance, recursively.
319319
(Contributed by Irit Katriel in :gh:`105292`.)
320320

321+
* Add the field *exc_type_str* to :class:`~traceback.TracebackException`, which
322+
holds a string display of the *exc_type*. Deprecate the field *exc_type*
323+
which holds the type object itself. Add parameter *save_exc_type* (default
324+
``True``) to indicate whether ``exc_type`` should be saved.
325+
(Contributed by Irit Katriel in :gh:`112332`.)
326+
321327
typing
322328
------
323329

@@ -377,6 +383,11 @@ Deprecated
377383
security and functionality bugs. This includes removal of the ``--cgi``
378384
flag to the ``python -m http.server`` command line in 3.15.
379385

386+
* :mod:`traceback`:
387+
388+
* The field *exc_type* of :class:`traceback.TracebackException` is
389+
deprecated. Use *exc_type_str* instead.
390+
380391
* :mod:`typing`:
381392

382393
* Creating a :class:`typing.NamedTuple` class using keyword arguments to denote

Lib/test/test_traceback.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2715,9 +2715,9 @@ def __repr__(self) -> str:
27152715

27162716
class TestTracebackException(unittest.TestCase):
27172717

2718-
def test_smoke(self):
2718+
def do_test_smoke(self, exc, expected_type_str):
27192719
try:
2720-
1/0
2720+
raise exc
27212721
except Exception as e:
27222722
exc_obj = e
27232723
exc = traceback.TracebackException.from_exception(e)
@@ -2727,9 +2727,23 @@ def test_smoke(self):
27272727
self.assertEqual(None, exc.__context__)
27282728
self.assertEqual(False, exc.__suppress_context__)
27292729
self.assertEqual(expected_stack, exc.stack)
2730-
self.assertEqual(type(exc_obj), exc.exc_type)
2730+
with self.assertWarns(DeprecationWarning):
2731+
self.assertEqual(type(exc_obj), exc.exc_type)
2732+
self.assertEqual(expected_type_str, exc.exc_type_str)
27312733
self.assertEqual(str(exc_obj), str(exc))
27322734

2735+
def test_smoke_builtin(self):
2736+
self.do_test_smoke(ValueError(42), 'ValueError')
2737+
2738+
def test_smoke_user_exception(self):
2739+
class MyException(Exception):
2740+
pass
2741+
2742+
self.do_test_smoke(
2743+
MyException('bad things happened'),
2744+
('test.test_traceback.TestTracebackException.'
2745+
'test_smoke_user_exception.<locals>.MyException'))
2746+
27332747
def test_from_exception(self):
27342748
# Check all the parameters are accepted.
27352749
def foo():
@@ -2750,7 +2764,9 @@ def foo():
27502764
self.assertEqual(None, exc.__context__)
27512765
self.assertEqual(False, exc.__suppress_context__)
27522766
self.assertEqual(expected_stack, exc.stack)
2753-
self.assertEqual(type(exc_obj), exc.exc_type)
2767+
with self.assertWarns(DeprecationWarning):
2768+
self.assertEqual(type(exc_obj), exc.exc_type)
2769+
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
27542770
self.assertEqual(str(exc_obj), str(exc))
27552771

27562772
def test_cause(self):
@@ -2772,7 +2788,9 @@ def test_cause(self):
27722788
self.assertEqual(exc_context, exc.__context__)
27732789
self.assertEqual(True, exc.__suppress_context__)
27742790
self.assertEqual(expected_stack, exc.stack)
2775-
self.assertEqual(type(exc_obj), exc.exc_type)
2791+
with self.assertWarns(DeprecationWarning):
2792+
self.assertEqual(type(exc_obj), exc.exc_type)
2793+
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
27762794
self.assertEqual(str(exc_obj), str(exc))
27772795

27782796
def test_context(self):
@@ -2792,7 +2810,9 @@ def test_context(self):
27922810
self.assertEqual(exc_context, exc.__context__)
27932811
self.assertEqual(False, exc.__suppress_context__)
27942812
self.assertEqual(expected_stack, exc.stack)
2795-
self.assertEqual(type(exc_obj), exc.exc_type)
2813+
with self.assertWarns(DeprecationWarning):
2814+
self.assertEqual(type(exc_obj), exc.exc_type)
2815+
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
27962816
self.assertEqual(str(exc_obj), str(exc))
27972817

27982818
def test_long_context_chain(self):
@@ -2837,7 +2857,9 @@ def test_compact_with_cause(self):
28372857
self.assertEqual(None, exc.__context__)
28382858
self.assertEqual(True, exc.__suppress_context__)
28392859
self.assertEqual(expected_stack, exc.stack)
2840-
self.assertEqual(type(exc_obj), exc.exc_type)
2860+
with self.assertWarns(DeprecationWarning):
2861+
self.assertEqual(type(exc_obj), exc.exc_type)
2862+
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
28412863
self.assertEqual(str(exc_obj), str(exc))
28422864

28432865
def test_compact_no_cause(self):
@@ -2857,9 +2879,22 @@ def test_compact_no_cause(self):
28572879
self.assertEqual(exc_context, exc.__context__)
28582880
self.assertEqual(False, exc.__suppress_context__)
28592881
self.assertEqual(expected_stack, exc.stack)
2860-
self.assertEqual(type(exc_obj), exc.exc_type)
2882+
with self.assertWarns(DeprecationWarning):
2883+
self.assertEqual(type(exc_obj), exc.exc_type)
2884+
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
28612885
self.assertEqual(str(exc_obj), str(exc))
28622886

2887+
def test_no_save_exc_type(self):
2888+
try:
2889+
1/0
2890+
except Exception as e:
2891+
exc = e
2892+
2893+
te = traceback.TracebackException.from_exception(
2894+
exc, save_exc_type=False)
2895+
with self.assertWarns(DeprecationWarning):
2896+
self.assertIsNone(te.exc_type)
2897+
28632898
def test_no_refs_to_exception_and_traceback_objects(self):
28642899
try:
28652900
1/0

Lib/traceback.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import linecache
66
import sys
77
import textwrap
8+
import warnings
89
from contextlib import suppress
910

1011
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
@@ -719,7 +720,8 @@ class TracebackException:
719720
- :attr:`__suppress_context__` The *__suppress_context__* value from the
720721
original exception.
721722
- :attr:`stack` A `StackSummary` representing the traceback.
722-
- :attr:`exc_type` The class of the original traceback.
723+
- :attr:`exc_type` (deprecated) The class of the original traceback.
724+
- :attr:`exc_type_str` String display of exc_type
723725
- :attr:`filename` For syntax errors - the filename where the error
724726
occurred.
725727
- :attr:`lineno` For syntax errors - the linenumber where the error
@@ -737,7 +739,7 @@ class TracebackException:
737739

738740
def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
739741
lookup_lines=True, capture_locals=False, compact=False,
740-
max_group_width=15, max_group_depth=10, _seen=None):
742+
max_group_width=15, max_group_depth=10, save_exc_type=True, _seen=None):
741743
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
742744
# permit backwards compat with the existing API, otherwise we
743745
# need stub thunk objects just to glue it together.
@@ -754,12 +756,23 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
754756
_walk_tb_with_full_positions(exc_traceback),
755757
limit=limit, lookup_lines=lookup_lines,
756758
capture_locals=capture_locals)
757-
self.exc_type = exc_type
759+
760+
self._exc_type = exc_type if save_exc_type else None
761+
758762
# Capture now to permit freeing resources: only complication is in the
759763
# unofficial API _format_final_exc_line
760764
self._str = _safe_string(exc_value, 'exception')
761765
self.__notes__ = getattr(exc_value, '__notes__', None)
762766

767+
self._is_syntax_error = False
768+
self._have_exc_type = exc_type is not None
769+
if exc_type is not None:
770+
self.exc_type_qualname = exc_type.__qualname__
771+
self.exc_type_module = exc_type.__module__
772+
else:
773+
self.exc_type_qualname = None
774+
self.exc_type_module = None
775+
763776
if exc_type and issubclass(exc_type, SyntaxError):
764777
# Handle SyntaxError's specially
765778
self.filename = exc_value.filename
@@ -771,6 +784,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
771784
self.offset = exc_value.offset
772785
self.end_offset = exc_value.end_offset
773786
self.msg = exc_value.msg
787+
self._is_syntax_error = True
774788
elif exc_type and issubclass(exc_type, ImportError) and \
775789
getattr(exc_value, "name_from", None) is not None:
776790
wrong_name = getattr(exc_value, "name_from", None)
@@ -869,6 +883,24 @@ def from_exception(cls, exc, *args, **kwargs):
869883
"""Create a TracebackException from an exception."""
870884
return cls(type(exc), exc, exc.__traceback__, *args, **kwargs)
871885

886+
@property
887+
def exc_type(self):
888+
warnings.warn('Deprecated in 3.13. Use exc_type_str instead.',
889+
DeprecationWarning, stacklevel=2)
890+
return self._exc_type
891+
892+
@property
893+
def exc_type_str(self):
894+
if not self._have_exc_type:
895+
return None
896+
stype = self.exc_type_qualname
897+
smod = self.exc_type_module
898+
if smod not in ("__main__", "builtins"):
899+
if not isinstance(smod, str):
900+
smod = "<unknown>"
901+
stype = smod + '.' + stype
902+
return stype
903+
872904
def _load_lines(self):
873905
"""Private API. force all lines in the stack to be loaded."""
874906
for frame in self.stack:
@@ -901,18 +933,12 @@ def format_exception_only(self, *, show_group=False, _depth=0):
901933
"""
902934

903935
indent = 3 * _depth * ' '
904-
if self.exc_type is None:
936+
if not self._have_exc_type:
905937
yield indent + _format_final_exc_line(None, self._str)
906938
return
907939

908-
stype = self.exc_type.__qualname__
909-
smod = self.exc_type.__module__
910-
if smod not in ("__main__", "builtins"):
911-
if not isinstance(smod, str):
912-
smod = "<unknown>"
913-
stype = smod + '.' + stype
914-
915-
if not issubclass(self.exc_type, SyntaxError):
940+
stype = self.exc_type_str
941+
if not self._is_syntax_error:
916942
if _depth > 0:
917943
# Nested exceptions needs correct handling of multiline messages.
918944
formatted = _format_final_exc_line(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Deprecate the ``exc_type`` field of :class:`traceback.TracebackException`.
2+
Add ``exc_type_str`` to replace it.

0 commit comments

Comments
 (0)