Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bdaef6b
Fix a comment.
ericsnowcurrently Mar 7, 2024
6342635
Add PyInterpreterConfig helpers.
ericsnowcurrently Mar 21, 2024
c48dd00
Add config helpers to _testinternalcapi.
ericsnowcurrently Mar 21, 2024
0796fe9
Use the new helpers in run_in_subinterp_with_config().
ericsnowcurrently Mar 22, 2024
8993b41
Move the PyInterpreterConfig utils to their own file.
ericsnowcurrently Mar 22, 2024
a113017
_PyInterpreterState_ResolveConfig() -> _PyInterpreterConfig_InitFromS…
ericsnowcurrently Mar 22, 2024
fce72b8
_testinternalcapi.new_interpreter_config() -> _xxsubinterpreters.new_…
ericsnowcurrently Mar 22, 2024
c52e484
_testinternalcapi.get_interpreter_config() -> _xxsubinterpreters.get_…
ericsnowcurrently Mar 22, 2024
a2983ce
Call _PyInterpreterState_RequireIDRef() in _interpreters._incref().
ericsnowcurrently Mar 22, 2024
05a081e
_testinternalcapi.interpreter_incref() -> _interpreters._incref()
ericsnowcurrently Mar 23, 2024
8a39bbc
Supporting passing a config to _xxsubinterpreters.create().
ericsnowcurrently Mar 22, 2024
1173cd1
Factor out new_interpreter().
ericsnowcurrently Mar 22, 2024
92c11d3
Fix test_import.
ericsnowcurrently Mar 25, 2024
edda48d
Fix an outdent.
ericsnowcurrently Mar 25, 2024
5f617ed
Call _PyInterpreterState_RequireIDRef() in the right places.
ericsnowcurrently Apr 1, 2024
c504c79
Drop an unnecessary _PyInterpreterState_IDInitref() call.
ericsnowcurrently Apr 1, 2024
8a75c90
Reduce to just the new internal C-API.
ericsnowcurrently Apr 2, 2024
a38cda7
Adjust test_get_config.
ericsnowcurrently Apr 2, 2024
cae0482
Remove trailing whitespace.
ericsnowcurrently Apr 2, 2024
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
_testinternalcapi.new_interpreter_config() -> _xxsubinterpreters.new_…
…config()
  • Loading branch information
ericsnowcurrently committed Mar 23, 2024
commit fce72b818ae9c4c264df5c1bb9b2cc6b0ff0efd0
143 changes: 141 additions & 2 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import os
import pickle
import threading
from textwrap import dedent
import threading
import types
import unittest
try:
import _testinternalcapi
except ImportError:
_testinternalcapi = None

from test import support
from test.support import import_helper
# Raise SkipTest if subinterpreters not supported.
import_helper.import_module('_xxsubinterpreters')
_interpreters = import_helper.import_module('_xxsubinterpreters')
from test.support import interpreters
from test.support.interpreters import InterpreterNotFoundError
from .utils import _captured_script, _run_output, _running, TestBase


def requires__testinternalcapi(func):
return unittest.skipIf(_testinternalcapi is None, "test requires _testinternalcapi module")(func)


class ModuleTests(TestBase):

def test_queue_aliases(self):
Expand Down Expand Up @@ -932,6 +941,136 @@ class SubBytes(bytes):
interpreters.is_shareable(obj))


class LowLevelTests(TestBase):

# The behaviors in the low-level module are important in as much
# as they are exercised by the high-level module. Therefore the
# most important testing happens in the high-level tests.
# These low-level tests cover corner cases that are not
# encountered by the high-level module, thus they
# mostly shouldn't matter as much.

@requires__testinternalcapi
def test_new_config(self):
default = _interpreters.new_config('isolated')
with self.subTest('no arg'):
config = _interpreters.new_config()
self.assert_ns_equal(config, default)
self.assertIsNot(config, default)

with self.subTest('default'):
config1 = _interpreters.new_config('default')
self.assert_ns_equal(config1, default)
self.assertIsNot(config1, default)

config2 = _interpreters.new_config('default')
self.assert_ns_equal(config2, config1)
self.assertIsNot(config2, config1)

for arg in ['', 'default']:
with self.subTest(f'default ({arg!r})'):
config = _interpreters.new_config(arg)
self.assert_ns_equal(config, default)
self.assertIsNot(config, default)

supported = {
'isolated': types.SimpleNamespace(
use_main_obmalloc=False,
allow_fork=False,
allow_exec=False,
allow_threads=True,
allow_daemon_threads=False,
check_multi_interp_extensions=True,
gil='own',
),
'legacy': types.SimpleNamespace(
use_main_obmalloc=True,
allow_fork=True,
allow_exec=True,
allow_threads=True,
allow_daemon_threads=True,
check_multi_interp_extensions=False,
gil='shared',
),
'empty': types.SimpleNamespace(
use_main_obmalloc=False,
allow_fork=False,
allow_exec=False,
allow_threads=False,
allow_daemon_threads=False,
check_multi_interp_extensions=False,
gil='default',
),
}
gil_supported = ['default', 'shared', 'own']

for name, vanilla in supported.items():
with self.subTest(f'supported ({name})'):
expected = vanilla
config1 = _interpreters.new_config(name)
self.assert_ns_equal(config1, expected)
self.assertIsNot(config1, expected)

config2 = _interpreters.new_config(name)
self.assert_ns_equal(config2, config1)
self.assertIsNot(config2, config1)

with self.subTest(f'noop override ({name})'):
expected = vanilla
overrides = vars(vanilla)
config = _interpreters.new_config(name, **overrides)
self.assert_ns_equal(config, expected)

with self.subTest(f'override all ({name})'):
overrides = {k: not v for k, v in vars(vanilla).items()}
for gil in gil_supported:
if vanilla.gil == gil:
continue
overrides['gil'] = gil
expected = types.SimpleNamespace(**overrides)
config = _interpreters.new_config(name, **overrides)
self.assert_ns_equal(config, expected)

# Override individual fields.
for field, old in vars(vanilla).items():
if field == 'gil':
values = [v for v in gil_supported if v != old]
else:
values = [not old]
for val in values:
with self.subTest(f'{name}.{field} ({old!r} -> {val!r})'):
overrides = {field: val}
expected = types.SimpleNamespace(
**dict(vars(vanilla), **overrides),
)
config = _interpreters.new_config(name, **overrides)
self.assert_ns_equal(config, expected)

with self.subTest('extra override'):
with self.assertRaises(ValueError):
_interpreters.new_config(spam=True)

# Bad values for bool fields.
for field, value in vars(supported['empty']).items():
if field == 'gil':
continue
assert isinstance(value, bool)
for value in [1, '', 'spam', 1.0, None, object()]:
with self.subTest(f'bad override ({field}={value!r})'):
with self.assertRaises(TypeError):
_interpreters.new_config(**{field: value})

# Bad values for .gil.
for value in [True, 1, 1.0, None, object()]:
with self.subTest(f'bad override (gil={value!r})'):
with self.assertRaises(TypeError):
_interpreters.new_config(gil=value)
for value in ['', 'spam']:
with self.subTest(f'bad override (gil={value!r})'):
with self.assertRaises(ValueError):
_interpreters.new_config(gil=value)


if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports.
unittest.main()
21 changes: 19 additions & 2 deletions Lib/test/test_interpreters/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def run():

class TestBase(unittest.TestCase):

def tearDown(self):
clean_up_interpreters()

def pipe(self):
def ensure_closed(fd):
try:
Expand Down Expand Up @@ -156,5 +159,19 @@ def assert_python_failure(self, *argv):
self.assertNotEqual(exitcode, 0)
return stdout, stderr

def tearDown(self):
clean_up_interpreters()
def assert_ns_equal(self, ns1, ns2, msg=None):
# This is mostly copied from TestCase.assertDictEqual.
self.assertEqual(type(ns1), type(ns2))
if ns1 == ns2:
return

import difflib
import pprint
from unittest.util import _common_shorten_repr
standardMsg = '%s != %s' % _common_shorten_repr(ns1, ns2)
diff = ('\n' + '\n'.join(difflib.ndiff(
pprint.pformat(vars(ns1)).splitlines(),
pprint.pformat(vars(ns2)).splitlines())))
diff = f'namespace({diff})'
standardMsg = self._truncateMessage(standardMsg, diff)
self.fail(self._formatMessage(msg, standardMsg))
53 changes: 0 additions & 53 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1412,57 +1412,6 @@ get_interpreter_config(PyObject *self, PyObject *args)
return configobj;
}

static PyObject *
new_interpreter_config(PyObject *self, PyObject *args, PyObject *kwargs)
{
const char *initialized = NULL;
PyObject *overrides = NULL;
static char *kwlist[] = {"initialized", "overrides", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"|sO:new_interpreter_config", kwlist,
&initialized, &overrides))
{
return NULL;
}
if (initialized == NULL
|| strcmp(initialized, "") == 0
|| strcmp(initialized, "default") == 0)
{
initialized = "isolated";
}

PyInterpreterConfig config;
if (strcmp(initialized, "isolated") == 0) {
config = (PyInterpreterConfig)_PyInterpreterConfig_INIT;
}
else if (strcmp(initialized, "legacy") == 0) {
config = (PyInterpreterConfig)_PyInterpreterConfig_LEGACY_INIT;
}
else if (strcmp(initialized, "empty") == 0) {
config = (PyInterpreterConfig){0};
}
else {
PyErr_Format(PyExc_ValueError,
"unsupported initialized arg '%s'", initialized);
return NULL;
}

if (overrides != NULL) {
if (_PyInterpreterConfig_UpdateFromDict(&config, overrides) < 0) {
return NULL;
}
}

PyObject *dict = _PyInterpreterConfig_AsDict(&config);
if (dict == NULL) {
return NULL;
}

PyObject *configobj = _PyNamespace_New(dict);
Py_DECREF(dict);
return configobj;
}


/* To run some code in a sub-interpreter. */
static PyObject *
Expand Down Expand Up @@ -1903,8 +1852,6 @@ static PyMethodDef module_functions[] = {
{"hamt", new_hamt, METH_NOARGS},
{"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS},
{"get_interpreter_config", get_interpreter_config, METH_VARARGS},
{"new_interpreter_config", _PyCFunction_CAST(new_interpreter_config),
METH_VARARGS | METH_KEYWORDS},
{"run_in_subinterp_with_config",
_PyCFunction_CAST(run_in_subinterp_with_config),
METH_VARARGS | METH_KEYWORDS},
Expand Down
76 changes: 76 additions & 0 deletions Modules/_xxsubinterpretersmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
#include "pycore_initconfig.h" // _PyErr_SetFromPyStatus()
#include "pycore_long.h" // _PyLong_IsNegative()
#include "pycore_modsupport.h" // _PyArg_BadArgument()
#include "pycore_namespace.h" // _PyNamespace_New()
#include "pycore_pybuffer.h" // _PyBuffer_ReleaseInInterpreterAndRawFree()
#include "pycore_pyerrors.h" // _Py_excinfo
#include "pycore_pylifecycle.h" // _PyInterpreterConfig_AsDict()
#include "pycore_pystate.h" // _PyInterpreterState_SetRunningMain()

#include "marshal.h" // PyMarshal_ReadObjectFromString()
Expand Down Expand Up @@ -349,6 +351,34 @@ get_code_str(PyObject *arg, Py_ssize_t *len_p, PyObject **bytes_p, int *flags_p)

/* interpreter-specific code ************************************************/

static int
init_named_config(PyInterpreterConfig *config, const char *name)
{
if (name == NULL
|| strcmp(name, "") == 0
|| strcmp(name, "default") == 0)
{
name = "isolated";
}

if (strcmp(name, "isolated") == 0) {
*config = (PyInterpreterConfig)_PyInterpreterConfig_INIT;
}
else if (strcmp(name, "legacy") == 0) {
*config = (PyInterpreterConfig)_PyInterpreterConfig_LEGACY_INIT;
}
else if (strcmp(name, "empty") == 0) {
*config = (PyInterpreterConfig){0};
}
else {
PyErr_Format(PyExc_ValueError,
"unsupported config name '%s'", name);
return -1;
}
return 0;
}


static int
_run_script(PyObject *ns, const char *codestr, Py_ssize_t codestrlen, int flags)
{
Expand Down Expand Up @@ -417,6 +447,50 @@ _run_in_interpreter(PyInterpreterState *interp,

/* module level code ********************************************************/

static PyObject *
interp_new_config(PyObject *self, PyObject *args, PyObject *kwds)
{
const char *name = NULL;
if (!PyArg_ParseTuple(args, "|s:" MODULE_NAME_STR ".new_config",
&name))
{
return NULL;
}
PyObject *overrides = kwds;

PyInterpreterConfig config;
if (init_named_config(&config, name) < 0) {
return NULL;
}

if (overrides != NULL && PyDict_GET_SIZE(overrides) > 0) {
if (_PyInterpreterConfig_UpdateFromDict(&config, overrides) < 0) {
return NULL;
}
}

PyObject *dict = _PyInterpreterConfig_AsDict(&config);
if (dict == NULL) {
return NULL;
}

PyObject *configobj = _PyNamespace_New(dict);
Py_DECREF(dict);
return configobj;
}

PyDoc_STRVAR(new_config_doc,
"new_config(name='isolated', /, **overrides) -> type.SimpleNamespace\n\
\n\
Return a representation of a new PyInterpreterConfig.\n\
\n\
The name determines the initial values of the config. Supported named\n\
configs are: default, isolated, legacy, and empty.\n\
\n\
Any keyword arguments are set on the corresponding config fields,\n\
overriding the initial values.");


static PyObject *
interp_create(PyObject *self, PyObject *args, PyObject *kwds)
{
Expand Down Expand Up @@ -1033,6 +1107,8 @@ interp_decref(PyObject *self, PyObject *args, PyObject *kwds)


static PyMethodDef module_functions[] = {
{"new_config", _PyCFunction_CAST(interp_new_config),
METH_VARARGS | METH_KEYWORDS, new_config_doc},
{"create", _PyCFunction_CAST(interp_create),
METH_VARARGS | METH_KEYWORDS, create_doc},
{"destroy", _PyCFunction_CAST(interp_destroy),
Expand Down
Loading