8000 bpo-30439: Add some helpful low-level functions for subinterpreters. by ericsnowcurrently · Pull Request #1802 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-30439: Add some helpful low-level functions for subinterpreters. #1802

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

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8eb5adf
Add the _interpreters module to the stdlib.
ericsnowcurrently Dec 14, 2016
457567f
Add create() and destroy().
ericsnowcurrently Dec 29, 2016
e6af9f3
Finish nearly all the create/destroy tests.
ericsnowcurrently Jan 1, 2017
335967e
Add run_string().
ericsnowcurrently Dec 29, 2016
d2df973
Get tricky tests working.
ericsnowcurrently Jan 2, 2017
b70a496
Add a test for a still running interpreter when main exits.
ericsnowcurrently Jan 2, 2017
6f2d28f
Add run_string_unrestricted().
ericsnowcurrently Jan 4, 2017
80a931b
Exit out of the child process.
ericsnowcurrently Jan 4, 2017
6aa7d9c
Resolve several TODOs.
ericsnowcurrently Jan 4, 2017
083e13c
Set up the execution namespace before switching threads.
ericsnowcurrently Jan 4, 2017
ce081a7
Run in a copy of __main__.
ericsnowcurrently Jan 4, 2017
ec05cf5
Close stdin and stdout after the proc finishes.
ericsnowcurrently Jan 4, 2017
fe90466
Clean up a test.
ericsnowcurrently Jan 4, 2017
9082017
Chain exceptions during cleanup.
ericsnowcurrently Jan 4, 2017
21865d4
Finish the module docs.
ericsnowcurrently Jan 4, 2017
0342a4f
Fix docs.
ericsnowcurrently May 23, 2017
e9d9b04
Fix includes.
ericsnowcurrently Dec 5, 2017
722ae94
Add _interpreters.is_shareable().
ericsnowcurrently Nov 28, 2017
061ae13
Add _PyObject_CheckShareable().
ericsnowcurrently Dec 4, 2017
9a365ec
Add _PyCrossInterpreterData.
ericsnowcurrently Dec 4, 2017
8f299d4
Use the shared data in run() safely.
ericsnowcurrently Dec 7, 2017
92029a1
Do not use a copy of the __main__ ns.
ericsnowcurrently Dec 7, 2017
52c9c2f
Never return the execution namespace.
ericsnowcurrently Dec 8, 2017
a797883
Group sharing-related code.
ericsnowcurrently Dec 8, 2017
777838a
Fix a refcount.
ericsnowcurrently Dec 8, 2017
ab8f175
Add get_current() and enumerate().
ericsnowcurrently Dec 29, 2016
c50c51c
Add is_running().
ericsnowcurrently Dec 29, 2016
271d20d
Add get_main().
ericsnowcurrently Jan 4, 2017
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
Next Next commit
Add run_string().
  • Loading branch information
ericsnowcurrently committed Dec 5, 2017
commit 335967e0e36d60047992f0ef757a8c3c42d0d71f
18 changes: 17 additions & 1 deletion Doc/library/_interpreters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ support multiple interpreters.

It defines the following functions:


.. function:: create()

Initialize a new Python interpreter and return its identifier. The
Expand All @@ -42,6 +41,23 @@ It defines the following functions:
.. XXX must not be running?


.. function:: run_string(id, command)

A wrapper around :c:func:`PyRun_SimpleString` which runs the provided
Python program using the identified interpreter. Providing an
invalid or unknown ID results in a RuntimeError, likewise if the main
interpreter or any other running interpreter is used.

Any value returned from the code is thrown away, similar to what
threads do. If the code results in an exception then that exception
is raised in the thread in which run_string() was called, similar to
how :func:`exec` works. This aligns with how interpreters are not
inherently threaded.

.. XXX must not be running already?
.. XXX sys.exit() (and SystemExit) is swallowed?


**Caveats:**

* ...
Expand Down
190 changes: 177 additions & 13 deletions Lib/test/test__interpreters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import contextlib
import os
import os.path
import shutil
import tempfile
from textwrap import dedent
import threading
import unittest

Expand All @@ -11,14 +15,14 @@
@contextlib.contextmanager
def _blocked():
r, w = os.pipe()
wait_script = """if True:
wait_script = dedent("""
import select
# Wait for a "done" signal.
select.select([{}], [], [])

#import time
#time.sleep(1_000_000)
""".format(r)
""").format(r)
try:
yield wait_script
finally:
Expand Down Expand Up @@ -73,11 +77,10 @@ def f():
def test_in_subinterpreter(self):
main, = interpreters._enumerate()
id = interpreters.create()
interpreters._run_string(id, """if True:
interpreters.run_string(id, dedent("""
import _interpreters
id = _interpreters.create()
#_interpreters.create()
""")
"""))

ids = interpreters._enumerate()
self.assertIn(id, ids)
Expand All @@ -88,10 +91,10 @@ def test_in_threaded_subinterpreter(self):
main, = interpreters._enumerate()
id = interpreters.create()
def f():
interpreters._run_string(id, """if True:
interpreters.run_string(id, dedent("""
import _interpreters
_interpreters.create()
""")
"""))

t = threading.Thread(target=f)
t.start()
Expand All @@ -102,6 +105,7 @@ def f():
self.assertIn(main, ids)
self.assertEqual(len(ids), 3)


def test_after_destroy_all(self):
before = set(interpreters._enumerate())
# Create 3 subinterpreters.
Expand Down Expand Up @@ -183,19 +187,19 @@ def test_bad_id(self):
def test_from_current(self):
id = interpreters.create()
with self.assertRaises(RuntimeError):
interpreters._run_string(id, """if True:
interpreters.run_string(id, dedent("""
import _interpreters
_interpreters.destroy({})
""".format(id))
""").format(id))

def test_from_sibling(self):
main, = interpreters._enumerate()
id1 = interpreters.create()
id2 = interpreters.create()
interpreters._run_string(id1, """if True:
interpreters.run_string(id1, dedent("""
import _interpreters
_interpreters.destroy({})
""".format(id2))
""").format(id2))
self.assertEqual(set(interpreters._enumerate()), {main, id1})

def test_from_other_thread(self):
Expand All @@ -212,7 +216,7 @@ def test_still_running(self):
main, = interpreters._enumerate()
id = interpreters.create()
def f():
interpreters._run_string(id, wait_script)
interpreters.run_string(id, wait_script)

t = threading.Thread(target=f)
with _blocked() as wait_script:
Expand All @@ -224,5 +228,165 @@ def f():
self.assertEqual(set(interpreters._enumerate()), {main, id})


if __name__ == "__main__":
class RunStringTests(TestBase):

SCRIPT = dedent("""
with open('{}', 'w') as out:
out.write('{}')
""")
FILENAME = 'spam'

def setUp(self):
self.id = interpreters.create()
self.dirname = None
self.filename = None

def tearDown(self):
if self.dirname is not None:
shutil.rmtree(self.dirname)
super().tearDown()

def _resolve_filename(self, name=None):
if name is None:
name = self.FILENAME
if self.dirname is None:
self.dirname = tempfile.mkdtemp()
return os.path.join(self.dirname, name)

def _empty_file(self):
self.filename = self._resolve_filename()
support.create_empty_file(self.filename)
return self.filename

def assert_file_contains(self, expected, filename=None):
if filename is None:
filename = self.filename
self.assertIsNot(filename, None)
with open(filename) as out:
content = out.read()
self.assertEqual(content, expected)

def test_success(self):
filename = self._empty_file()
expected = 'spam spam spam spam spam'
script = self.SCRIPT.format(filename, expected)
interpreters.run_string(self.id, script)

self.assert_file_contains(expected)

def test_in_thread(self):
filename = self._empty_file()
expected = 'spam spam spam spam spam'
script = self.SCRIPT.format(filename, expected)
def f():
interpreters.run_string(self.id, script)

t = threading.Thread(target=f)
t.start()
t.join()

self.assert_file_contains(expected)

def test_create_thread(self):
filename = self._empty_file()
expected = 'spam spam spam spam spam'
script = dedent("""
import threading
def f():
with open('{}', 'w') as out:
out.write('{}')

t = threading.Thread(target=f)
t.start()
t.join()
""").format(filename, expected)
interpreters.run_string(self.id, script)

self.assert_file_contains(expected)

@unittest.skip('not working yet')
@unittest.skipUnless(hasattr(os, 'fork'), "test needs os.fork()")
def test_fork(self):
filename = self._empty_file()
expected = 'spam spam spam spam spam'
script = dedent("""
import os
import sys
pid = os.fork()
if pid == 0:
with open('{}', 'w') as out:
out.write('{}')
sys.exit(0)
""").format(filename, expected)
interpreters.run_string(self.id, script)

self.assert_file_contains(expected)

@unittest.skip('not working yet')
def test_already_running(self):
def f():
interpreters.run_string(self.id, wait_script)

t = threading.Thread(target=f)
with _blocked() as wait_script:
t.start()
with self.assertRaises(RuntimeError):
interpreters.run_string(self.id, 'print("spam")')
t.join()

def test_does_not_exist(self):
id = 0
while id in interpreters._enumerate():
id += 1
with self.assertRaises(RuntimeError):
interpreters.run_string(id, 'print("spam")')

def test_error_id(self):
with self.assertRaises(RuntimeError):
interpreters.run_string(-1, 'print("spam")')

def test_bad_id(self):
with self.assertRaises(TypeError):
interpreters.run_string('spam', 'print("spam")')

def test_bad_code(self):
with self.assertRaises(TypeError):
interpreters.run_string(self.id, 10)

def test_bytes_for_code(self):
with self.assertRaises(TypeError):
interpreters.run_string(self.id, b'print("spam")')

def test_invalid_syntax(self):
with self.assertRaises(SyntaxError):
# missing close paren
interpreters.run_string(self.id, 'print("spam"')

def test_failure(self):
with self.assertRaises(Exception) as caught:
interpreters.run_string(self.id, 'raise Exception("spam")')
self.assertEqual(str(caught.exception), 'spam')

def test_sys_exit(self):
with self.assertRaises(SystemExit) as cm:
interpreters.run_string(self.id, dedent("""
import sys
sys.exit()
"""))
self.assertIsNone(cm.exception.code)

with self.assertRaises(SystemExit) as cm:
interpreters.run_string(self.id, dedent("""
import sys
sys.exit(42)
"""))
self.assertEqual(cm.exception.code, 42)

def test_SystemError(self):
with self.assertRaises(SystemExit) as cm:
interpreters.run_string(self.id, 'raise SystemExit(42)')
self.assertEqual(cm.exception.code, 42)


if __name__ == '__main__':
unittest.main()
11 changes: 9 additions & 2 deletions Modules/_interpretersmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ interp_run_string(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

PyDoc_STRVAR(run_string_doc,
"run_string(ID, sourcetext) -> run_id\n\
\n\
Execute the provided string in the identified interpreter.\n\
See PyRun_SimpleStrings.");


static PyMethodDef module_functions[] = {
{"create", (PyCFunction)interp_create,
METH_VARARGS, create_doc},
Expand All @@ -288,8 +295,8 @@ static PyMethodDef module_functions[] = {
{"_enumerate", (PyCFunction)interp_enumerate,
METH_NOARGS, NULL},

{"_run_string", (PyCFunction)interp_run_string,
METH_VARARGS, NULL},
{"run_string", (PyCFunction)interp_run_string,
METH_VARARGS, run_string_doc},

{NULL, NULL} /* sentinel */
};
Expand Down
0