summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorEric Snow <ericsnowcurrently@gmail.com>2023-12-13 00:00:54 (GMT)
committerGitHub <noreply@github.com>2023-12-13 00:00:54 (GMT)
commit8a4c1f3ff1e3d7ed2e00e77b94056f9bb7f9ae3b (patch)
tree223a5a4761058341672e81141cf2ce6c90bce16d /Lib
parent7316dfb0ebc46aedf484c1f15f03a0a309d12a42 (diff)
downloadcpython-8a4c1f3ff1e3d7ed2e00e77b94056f9bb7f9ae3b.zip
cpython-8a4c1f3ff1e3d7ed2e00e77b94056f9bb7f9ae3b.tar.gz
cpython-8a4c1f3ff1e3d7ed2e00e77b94056f9bb7f9ae3b.tar.bz2
gh-76785: Show the Traceback for Uncaught Subinterpreter Exceptions (gh-113034)
When an exception is uncaught in Interpreter.exec_sync(), it helps to show that exception's error display if uncaught in the calling interpreter. We do so here by generating a TracebackException in the subinterpreter and passing it between interpreters using pickle.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/support/interpreters/__init__.py27
-rw-r--r--Lib/test/test_interpreters/test_api.py48
-rw-r--r--Lib/test/test_interpreters/utils.py72
3 files changed, 143 insertions, 4 deletions
diff --git a/Lib/test/support/interpreters/__init__.py b/Lib/test/support/interpreters/__init__.py
index 9cd1c3d..d619bea 100644
--- a/Lib/test/support/interpreters/__init__.py
+++ b/Lib/test/support/interpreters/__init__.py
@@ -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():
diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py
index b702338..aefd326 100644
--- a/Lib/test/test_interpreters/test_api.py
+++ b/Lib/test/test_interpreters/test_api.py
@@ -525,6 +525,54 @@ class TestInterpreterExecSync(TestBase):
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="")')
diff --git a/Lib/test/test_interpreters/utils.py b/Lib/test/test_interpreters/utils.py
index 11b6f12..3a37ed0 100644
--- a/Lib/test/test_interpreters/utils.py
+++ b/Lib/test/test_interpreters/utils.py
@@ -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
@@ -71,5 +78,70 @@ class TestBase(unittest.TestCase):
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()