diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 259f70f1ea0dbc..96510eeec54640 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -18,7 +18,7 @@ from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ, requires_debug_ranges, has_no_debug_ranges, requires_subprocess) -from test.support.os_helper import TESTFN, unlink +from test.support.os_helper import TESTFN, temp_dir, unlink from test.support.script_helper import assert_python_ok, assert_python_failure, make_script from test.support.import_helper import forget from test.support import force_not_colorized, force_not_colorized_test_class @@ -524,6 +524,33 @@ def __del__(self): b'ZeroDivisionError: division by zero'] self.assertEqual(stderr.splitlines(), expected) + @cpython_only + def test_lost_io_open(self): + # GH-142737: Display the traceback even if io.open is lost + crasher = textwrap.dedent("""\ + import io + import traceback + # Trigger fallback mode + traceback._print_exception_bltin = None + del io.open + raise RuntimeError("should not crash") + """) + + # Create a temporary script to exercise _Py_FindSourceFile + with temp_dir() as script_dir: + script = make_script( + script_dir=script_dir, + script_basename='tb_test_no_io_open', + source=crasher) + rc, stdout, stderr = assert_python_failure(script) + + self.assertEqual(rc, 1) # Make sure it's not a crash + + expected = [b'Traceback (most recent call last):', + f' File "{script}", line 6, in '.encode(), + b'RuntimeError: should not crash'] + self.assertEqual(stderr.splitlines(), expected) + def test_print_exception(self): output = StringIO() traceback.print_exception( diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-15-01-21.gh-issue-142737.xYXzeB.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-15-01-21.gh-issue-142737.xYXzeB.rst new file mode 100644 index 00000000000000..8b743d1e49de21 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-15-15-01-21.gh-issue-142737.xYXzeB.rst @@ -0,0 +1,3 @@ +Tracebacks will be displayed in fallback mode even if :func:`io.open` is lost. +Previously, this would crash the interpreter. +Patch by Bartosz Sławecki. diff --git a/Objects/call.c b/Objects/call.c index 41d075caf11ce6..af42fc8f7f2dbf 100644 --- a/Objects/call.c +++ b/Objects/call.c @@ -729,6 +729,7 @@ _Py_COMP_DIAG_POP PyObject * _PyObject_CallMethodFormat(PyThreadState *tstate, PyObject *callable, const char *format, ...) { + assert(callable != NULL); va_list va; va_start(va, format); PyObject *retval = callmethod(tstate, callable, format, va); diff --git a/Python/traceback.c b/Python/traceback.c index 8af63c22a9f84e..264f034dea7fa5 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -415,6 +415,9 @@ _Py_FindSourceFile(PyObject *filename, char* namebuf, size_t namelen, PyObject * npath = PyList_Size(syspath); open = PyObject_GetAttr(io, &_Py_ID(open)); + if (open == NULL) { + goto error; + } for (i = 0; i < npath; i++) { v = PyList_GetItem(syspath, i); if (v == NULL) {