Skip to content

gh-76785: Show the Traceback for Uncaught Subinterpreter Exceptions #113034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Display the propagated exception with uncaught ExecFailure.
  • Loading branch information
ericsnowcurrently committed Dec 12, 2023
commit b4378bd11563523e845bca4bab0e9f7774e2b9d1
27 changes: 23 additions & 4 deletions Lib/test/support/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,36 @@ def __getattr__(name):
raise AttributeError(name)


_EXEC_FAILURE_STR = """
{superstr}

Uncaught in the interpreter:

{formatted}
""".strip()

class ExecFailure(RuntimeError):

def __init__(self, excinfo):
msg = excinfo.formatted
if not msg:
if excinfo.type and snapshot.msg:
msg = f'{snapshot.type.__name__}: {snapshot.msg}'
if excinfo.type and excinfo.msg:
msg = f'{excinfo.type.__name__}: {excinfo.msg}'
else:
msg = snapshot.type.__name__ or snapshot.msg
msg = excinfo.type.__name__ or excinfo.msg
super().__init__(msg)
self.snapshot = excinfo
self.excinfo = excinfo

def __str__(self):
try:
formatted = ''.join(self.excinfo.tbexc.format()).rstrip()
except Exception:
return super().__str__()
else:
return _EXEC_FAILURE_STR.format(
superstr=super().__str__(),
formatted=formatted,
)


def create():
Expand Down
48 changes: 48 additions & 0 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,54 @@ def test_failure(self):
with self.assertRaises(interpreters.ExecFailure):
interp.exec_sync('raise Exception')

def test_display_preserved_exception(self):
tempdir = self.temp_dir()
modfile = self.make_module('spam', tempdir, text="""
def ham():
raise RuntimeError('uh-oh!')

def eggs():
ham()
""")
scriptfile = self.make_script('script.py', tempdir, text="""
from test.support import interpreters

def script():
import spam
spam.eggs()

interp = interpreters.create()
interp.exec_sync(script)
""")

stdout, stderr = self.assert_python_failure(scriptfile)
self.maxDiff = None
interpmod_line, = (l for l in stderr.splitlines() if ' exec_sync' in l)
# File "{interpreters.__file__}", line 179, in exec_sync
self.assertEqual(stderr, dedent(f"""\
Traceback (most recent call last):
File "{scriptfile}", line 9, in <module>
interp.exec_sync(script)
~~~~~~~~~~~~~~~~^^^^^^^^
{interpmod_line.strip()}
raise ExecFailure(excinfo)
test.support.interpreters.ExecFailure: RuntimeError: uh-oh!

Uncaught in the interpreter:

Traceback (most recent call last):
File "{scriptfile}", line 6, in script
spam.eggs()
~~~~~~~~~^^
File "{modfile}", line 6, in eggs
ham()
~~~^^
File "{modfile}", line 3, in ham
raise RuntimeError('uh-oh!')
RuntimeError: uh-oh!
"""))
self.assertEqual(stdout, '')

def test_in_thread(self):
interp = interpreters.create()
script, file = _captured_script('print("it worked!", end="")')
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_interpreters/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import contextlib
import os
import os.path
import subprocess
import sys
import tempfile
import threading
from textwrap import dedent
import unittest

from test import support
from test.support import os_helper

from test.support import interpreters


Expand Down Expand Up @@ -71,5 +78,70 @@ def ensure_closed(fd):
self.addCleanup(lambda: ensure_closed(w))
return r, w

def temp_dir(self):
tempdir = tempfile.mkdtemp()
tempdir = os.path.realpath(tempdir)
self.addCleanup(lambda: os_helper.rmtree(tempdir))
return tempdir

def make_script(self, filename, dirname=None, text=None):
if text:
text = dedent(text)
if dirname is None:
dirname = self.temp_dir()
filename = os.path.join(dirname, filename)

os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, 'w', encoding='utf-8') as outfile:
outfile.write(text or '')
return filename

def make_module(self, name, pathentry=None, text=None):
if text:
text = dedent(text)
if pathentry is None:
pathentry = self.temp_dir()
else:
os.makedirs(pathentry, exist_ok=True)
*subnames, basename = name.split('.')

dirname = pathentry
for subname in subnames:
dirname = os.path.join(dirname, subname)
if os.path.isdir(dirname):
pass
elif os.path.exists(dirname):
raise Exception(dirname)
else:
os.mkdir(dirname)
initfile = os.path.join(dirname, '__init__.py')
if not os.path.exists(initfile):
with open(initfile, 'w'):
pass
filename = os.path.join(dirname, basename + '.py')

with open(filename, 'w', encoding='utf-8') as outfile:
outfile.write(text or '')
return filename

@support.requires_subprocess()
def run_python(self, *argv):
proc = subprocess.run(
[sys.executable, *argv],
capture_output=True,
text=True,
)
return proc.returncode, proc.stdout, proc.stderr

def assert_python_ok(self, *argv):
exitcode, stdout, stderr = self.run_python(*argv)
self.assertNotEqual(exitcode, 1)
return stdout, stderr

def assert_python_failure(self, *argv):
exitcode, stdout, stderr = self.run_python(*argv)
self.assertNotEqual(exitcode, 0)
return stdout, stderr

def tearDown(self):
clean_up_interpreters()
13 changes: 1 addition & 12 deletions Python/crossinterp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,7 @@ _capture_current_exception(_PyXI_session *session)
}
else {
failure = _PyXI_InitError(err, excval, _PyXI_ERR_UNCAUGHT_EXCEPTION);
Py_DECREF(excval);
if (failure == NULL && override != NULL) {
err->code = errcode;
}
Expand All @@ -2202,18 +2203,6 @@ _capture_current_exception(_PyXI_session *session)
err = NULL;
}

// a temporary hack (famous last words)
if (excval != NULL) {
// XXX Store the traceback info (or rendered traceback) on
// _PyXI_excinfo, attach it to the exception when applied,
// and teach PyErr_Display() to print it.
#ifdef Py_DEBUG
// XXX Drop this once _Py_excinfo picks up the slack.
PyErr_Display(NULL, excval, NULL);
#endif
Py_DECREF(excval);
}

// Finished!
assert(!PyErr_Occurred());
session->error = err;
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy