Skip to content
Closed
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
Next Next commit
Add run_string().
  • Loading branch information
ericsnowcurrently committed May 23, 2017
commit 189a2fd27d5de6d28752d798a681d37ee4a07e33
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