Skip to content

Commit e733136

Browse files
authored
gh-110721: Use the traceback module for PyErr_Display() and fallback to the C implementation (#110702)
1 parent 8c6c14b commit e733136

File tree

7 files changed

+146
-837
lines changed

7 files changed

+146
-837
lines changed

Include/internal/pycore_traceback.h

+2-3
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,8 @@ extern PyObject* _PyTraceBack_FromFrame(
9595

9696
/* Write the traceback tb to file f. Prefix each line with
9797
indent spaces followed by the margin (if it is not NULL). */
98-
extern int _PyTraceBack_Print_Indented(
99-
PyObject *tb, int indent, const char* margin,
100-
const char *header_margin, const char *header, PyObject *f);
98+
extern int _PyTraceBack_Print(
99+
PyObject *tb, const char *header, PyObject *f);
101100
extern int _Py_WriteIndentedMargin(int, const char*, PyObject *);
102101
extern int _Py_WriteIndent(int, PyObject *);
103102

Lib/test/test_sys.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ def test_excepthook_bytes_filename(self):
175175

176176
def test_excepthook(self):
177177
with test.support.captured_output("stderr") as stderr:
178-
sys.excepthook(1, '1', 1)
178+
with test.support.catch_unraisable_exception():
179+
sys.excepthook(1, '1', 1)
179180
self.assertTrue("TypeError: print_exception(): Exception expected for " \
180181
"value, str found" in stderr.getvalue())
181182

Lib/test/test_traceback.py

+41-14
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,9 @@ class CPythonTracebackLegacyErrorCaretTests(
963963
Same set of tests as above but with Python's legacy internal traceback printing.
964964
"""
965965

966-
class TracebackFormatTests(unittest.TestCase):
966+
967+
class TracebackFormatMixin:
968+
DEBUG_RANGES = True
967969

968970
def some_exception(self):
969971
raise KeyError('blah')
@@ -1137,6 +1139,8 @@ def g(count=10):
11371139
)
11381140
expected = (tb_line + result_g).splitlines()
11391141
actual = stderr_g.getvalue().splitlines()
1142+
if not self.DEBUG_RANGES:
1143+
expected = [line for line in expected if not set(line.strip()) == {"^"}]
11401144
self.assertEqual(actual, expected)
11411145

11421146
# Check 2 different repetitive sections
@@ -1173,6 +1177,8 @@ def h(count=10):
11731177
)
11741178
expected = (result_h + result_g).splitlines()
11751179
actual = stderr_h.getvalue().splitlines()
1180+
if not self.DEBUG_RANGES:
1181+
expected = [line for line in expected if not set(line.strip()) == {"^"}]
11761182
self.assertEqual(actual, expected)
11771183

11781184
# Check the boundary conditions. First, test just below the cutoff.
@@ -1199,11 +1205,13 @@ def h(count=10):
11991205
)
12001206
tb_line = (
12011207
'Traceback (most recent call last):\n'
1202-
f' File "{__file__}", line {lineno_g+77}, in _check_recursive_traceback_display\n'
1208+
f' File "{__file__}", line {lineno_g+81}, in _check_recursive_traceback_display\n'
12031209
' g(traceback._RECURSIVE_CUTOFF)\n'
12041210
)
12051211
expected = (tb_line + result_g).splitlines()
12061212
actual = stderr_g.getvalue().splitlines()
1213+
if not self.DEBUG_RANGES:
1214+
expected = [line for line in expected if not set(line.strip()) == {"^"}]
12071215
self.assertEqual(actual, expected)
12081216

12091217
# Second, test just above the cutoff.
@@ -1231,24 +1239,24 @@ def h(count=10):
12311239
)
12321240
tb_line = (
12331241
'Traceback (most recent call last):\n'
1234-
f' File "{__file__}", line {lineno_g+108}, in _check_recursive_traceback_display\n'
1242+
f' File "{__file__}", line {lineno_g+114}, in _check_recursive_traceback_display\n'
12351243
' g(traceback._RECURSIVE_CUTOFF + 1)\n'
12361244
)
12371245
expected = (tb_line + result_g).splitlines()
12381246
actual = stderr_g.getvalue().splitlines()
1247+
if not self.DEBUG_RANGES:
1248+
expected = [line for line in expected if not set(line.strip()) == {"^"}]
12391249
self.assertEqual(actual, expected)
12401250

12411251
@requires_debug_ranges()
1242-
def test_recursive_traceback_python(self):
1243-
self._check_recursive_traceback_display(traceback.print_exc)
1244-
1245-
@cpython_only
1246-
@requires_debug_ranges()
1247-
def test_recursive_traceback_cpython_internal(self):
1248-
from _testcapi import exception_print
1249-
def render_exc():
1250-
exception_print(sys.exception())
1251-
self._check_recursive_traceback_display(render_exc)
1252+
def test_recursive_traceback(self):
1253+
if self.DEBUG_RANGES:
1254+
self._check_recursive_traceback_display(traceback.print_exc)
1255+
else:
1256+
from _testcapi import exception_print
1257+
def render_exc():
1258+
exception_print(sys.exception())
1259+
self._check_recursive_traceback_display(render_exc)
12521260

12531261
def test_format_stack(self):
12541262
def fmt():
@@ -1321,7 +1329,8 @@ def test_exception_group_deep_recursion_traceback(self):
13211329
def test_print_exception_bad_type_capi(self):
13221330
from _testcapi import exception_print
13231331
with captured_output("stderr") as stderr:
1324-
exception_print(42)
1332+
with support.catch_unraisable_exception():
1333+
exception_print(42)
13251334
self.assertEqual(
13261335
stderr.getvalue(),
13271336
('TypeError: print_exception(): '
@@ -1345,6 +1354,24 @@ def test_print_exception_bad_type_python(self):
13451354
boundaries = re.compile(
13461355
'(%s|%s)' % (re.escape(cause_message), re.escape(context_message)))
13471356

1357+
class TestTracebackFormat(unittest.TestCase, TracebackFormatMixin):
1358+
pass
1359+
1360+
@cpython_only
1361+
class TestFallbackTracebackFormat(unittest.TestCase, TracebackFormatMixin):
1362+
DEBUG_RANGES = False
1363+
def setUp(self) -> None:
1364+
self.original_unraisable_hook = sys.unraisablehook
1365+
sys.unraisablehook = lambda *args: None
1366+
self.original_hook = traceback._print_exception_bltin
1367+
traceback._print_exception_bltin = lambda *args: 1/0
1368+
return super().setUp()
1369+
1370+
def tearDown(self) -> None:
1371+
traceback._print_exception_bltin = self.original_hook
1372+
sys.unraisablehook = self.original_unraisable_hook
1373+
return super().tearDown()
1374+
13481375
class BaseExceptionReportingTests:
13491376

13501377
def get_exception(self, exception_or_callable):

Lib/traceback.py

+21-5
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
125125
te.print(file=file, chain=chain)
126126

127127

128+
BUILTIN_EXCEPTION_LIMIT = object()
129+
130+
131+
def _print_exception_bltin(exc, /):
132+
file = sys.stderr if sys.stderr is not None else sys.__stderr__
133+
return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file)
134+
135+
128136
def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
129137
chain=True):
130138
"""Format a stack trace and the exception information.
@@ -406,12 +414,16 @@ def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None,
406414
# (frame, (lineno, end_lineno, colno, end_colno)) in the stack.
407415
# Only lineno is required, the remaining fields can be None if the
408416
# information is not available.
409-
if limit is None:
417+
builtin_limit = limit is BUILTIN_EXCEPTION_LIMIT
418+
if limit is None or builtin_limit:
410419
limit = getattr(sys, 'tracebacklimit', None)
411420
if limit is not None and limit < 0:
412421
limit = 0
413422
if limit is not None:
414-
if limit >= 0:
423+
if builtin_limit:
424+
frame_gen = tuple(frame_gen)
425+
frame_gen = frame_gen[len(frame_gen) - limit:]
426+
elif limit >= 0:
415427
frame_gen = itertools.islice(frame_gen, limit)
416428
else:
417429
frame_gen = collections.deque(frame_gen, maxlen=-limit)
@@ -741,9 +753,9 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
741753
wrong_name = getattr(exc_value, "name", None)
742754
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
743755
if suggestion:
744-
self._str += f" Or did you forget to import '{wrong_name}'"
756+
self._str += f" Or did you forget to import '{wrong_name}'?"
745757
else:
746-
self._str += f". Did you forget to import '{wrong_name}'"
758+
self._str += f". Did you forget to import '{wrong_name}'?"
747759
if lookup_lines:
748760
self._load_lines()
749761
self.__suppress_context__ = \
@@ -904,7 +916,11 @@ def _format_syntax_error(self, stype):
904916
if self.offset is not None:
905917
offset = self.offset
906918
end_offset = self.end_offset if self.end_offset not in {None, 0} else offset
907-
if offset == end_offset or end_offset == -1:
919+
if self.text and offset > len(self.text):
920+
offset = len(self.text) + 1
921+
if self.text and end_offset > len(self.text):
922+
end_offset = len(self.text) + 1
923+
if offset >= end_offset or end_offset < 0:
908924
end_offset = offset + 1
909925

910926
# Convert 1-based column offset to 0-based index into stripped text
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Use the :mod:`traceback` implementation for the default
2+
:c:func:`PyErr_Display` functionality. Patch by Pablo Galindo

0 commit comments

Comments
 (0)