Skip to content

gh-98608: Change _Py_NewInterpreter() to _Py_NewInterpreterFromConfig() #98609

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 16 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 0 additions & 2 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1571,8 +1571,6 @@ Private provisional API:

* :c:member:`PyConfig._init_main`: if set to ``0``,
:c:func:`Py_InitializeFromConfig` stops at the "Core" initialization phase.
* :c:member:`PyConfig._isolated_interpreter`: if non-zero,
disallow threads, subprocesses and fork.

.. c:function:: PyStatus _Py_InitializeMain(void)

Expand Down
19 changes: 15 additions & 4 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,6 @@ typedef struct PyConfig {
// If equal to 0, stop Python initialization before the "main" phase.
int _init_main;

// If non-zero, disallow threads, subprocesses, and fork.
// Default: 0.
int _isolated_interpreter;

// If non-zero, we believe we're running from a source tree.
int _is_python_build;
} PyConfig;
Expand Down Expand Up @@ -245,6 +241,21 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config,
Py_ssize_t length, wchar_t **items);


/* --- PyInterpreterConfig ------------------------------------ */

typedef struct {
int allow_fork;
int allow_subprocess;
int allow_threads;
} _PyInterpreterConfig;

#define _PyInterpreterConfig_LEGACY_INIT \
{ \
.allow_fork = 1, \
.allow_subprocess = 1, \
.allow_threads = 1, \
}

/* --- Helper functions --------------------------------------- */

/* Get the original command line arguments, before Python modified them.
Expand Down
3 changes: 2 additions & 1 deletion Include/cpython/pylifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ PyAPI_FUNC(int) _Py_CoerceLegacyLocale(int warn);
PyAPI_FUNC(int) _Py_LegacyLocaleDetected(int warn);
PyAPI_FUNC(char *) _Py_SetLocaleFromEnv(int category);

PyAPI_FUNC(PyThreadState *) _Py_NewInterpreter(int isolated_subinterpreter);
PyAPI_FUNC(PyThreadState *) _Py_NewInterpreterFromConfig(
const _PyInterpreterConfig *);
27 changes: 27 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,38 @@
#endif


/*
Runtime Feature Flags

Each flag indicate whether or not a specific runtime feature
is available in a given context. For example, forking the process
might not be allowed in the current interpreter (i.e. os.fork() would fail).
*/

// We leave the first 10 for less-specific features.

/* Set if threads are allowed. */
#define Py_RTFLAGS_THREADS (1UL << 10)

/* Set if os.fork() is allowed. */
#define Py_RTFLAGS_FORK (1UL << 15)

/* Set if subprocesses are allowed. */
#define Py_RTFLAGS_SUBPROCESS (1UL << 16)


PyAPI_FUNC(int) _PyInterpreterState_HasFeature(PyInterpreterState *interp,
unsigned long feature);


/* private interpreter helpers */

PyAPI_FUNC(int) _PyInterpreterState_RequiresIDRef(PyInterpreterState *);
PyAPI_FUNC(void) _PyInterpreterState_RequireIDRef(PyInterpreterState *, int);

PyAPI_FUNC(PyObject *) _PyInterpreterState_GetMainModule(PyInterpreterState *);


/* State unique per thread */

/* Py_tracefunc return -1 when raising an exception, or 0 for success. */
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ struct _is {
#ifdef HAVE_DLOPEN
int dlopenflags;
#endif
unsigned long feature_flags;

PyObject *dict; /* Stores per-interpreter state */

Expand Down
1 change: 0 additions & 1 deletion Lib/test/_test_embed_set_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def test_set_invalid(self):
'skip_source_first_line',
'_install_importlib',
'_init_main',
'_isolated_interpreter',
]
if MS_WINDOWS:
options.append('legacy_windows_stdio')
Expand Down
18 changes: 16 additions & 2 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,22 @@ def run_in_subinterp(code):
Run code in a subinterpreter. Raise unittest.SkipTest if the tracemalloc
module is enabled.
"""
_check_tracemalloc()
import _testcapi
return _testcapi.run_in_subinterp(code)


def run_in_subinterp_with_config(code, **config):
"""
Run code in a subinterpreter. Raise unittest.SkipTest if the tracemalloc
module is enabled.
"""
_check_tracemalloc()
import _testcapi
return _testcapi.run_in_subinterp_with_config(code, **config)


def _check_tracemalloc():
# Issue #10915, #15751: PyGILState_*() functions don't work with
# sub-interpreters, the tracemalloc module uses these functions internally
try:
Expand All @@ -1804,8 +1820,6 @@ def run_in_subinterp(code):
raise unittest.SkipTest("run_in_subinterp() cannot be used "
"if tracemalloc module is tracing "
"memory allocations")
import _testcapi
return _testcapi.run_in_subinterp(code)


def check_free_after_iterating(test, iter, cls, args=()):
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,45 @@ def test_py_config_isoloated_per_interpreter(self):
# test fails, assume that the environment in this process may
# be altered and suspect.

@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
def test_configured_settings(self):
"""
The config with which an interpreter is created corresponds
1-to-1 with the new interpreter's settings. This test verifies
that they match.
"""
import json

THREADS = 1<<10
FORK = 1<<15
SUBPROCESS = 1<<16

features = ['fork', 'subprocess', 'threads']
kwlist = [f'allow_{n}' for n in features]
for config, expected in {
(True, True, True): FORK | SUBPROCESS | THREADS,
(False, False, False): 0,
(False, True, True): SUBPROCESS | THREADS,
}.items():
kwargs = dict(zip(kwlist, config))
expected = {
'feature_flags': expected,
}
with self.subTest(config):
r, w = os.pipe()
script = textwrap.dedent(f'''
import _testinternalcapi, json, os
settings = _testinternalcapi.get_interp_settings()
with os.fdopen({w}, "w") as stdin:
json.dump(settings, stdin)
''')
with os.fdopen(r) as stdout:
support.run_in_subinterp_with_config(script, **kwargs)
out = stdout.read()
settings = json.loads(out)

self.assertEqual(settings, expected)

def test_mutate_exception(self):
"""
Exceptions saved in global module state get shared between
Expand Down
22 changes: 19 additions & 3 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'check_hash_pycs_mode': 'default',
'pathconfig_warnings': 1,
'_init_main': 1,
'_isolated_interpreter': 0,
'use_frozen_modules': not support.Py_DEBUG,
'safe_path': 0,
'_is_python_build': IGNORE_CONFIG,
Expand Down Expand Up @@ -881,8 +880,6 @@ def test_init_from_config(self):

'check_hash_pycs_mode': 'always',
'pathconfig_warnings': 0,

'_isolated_interpreter': 1,
}
self.check_all_configs("test_init_from_config", config, preconfig,
api=API_COMPAT)
Expand Down Expand Up @@ -1650,6 +1647,25 @@ def test_init_use_frozen_modules(self):
self.check_all_configs("test_init_use_frozen_modules", config,
api=API_PYTHON, env=env)

def test_init_main_interpreter_settings(self):
THREADS = 1<<10
FORK = 1<<15
SUBPROCESS = 1<<16
expected = {
# All optional features should be enabled.
'feature_flags': THREADS | FORK | SUBPROCESS,
}
out, err = self.run_embedded_interpreter(
'test_init_main_interpreter_settings',
)
self.assertEqual(err, '')
try:
out = json.loads(out)
except json.JSONDecodeError:
self.fail(f'fail to decode stdout: {out!r}')

self.assertEqual(out, expected)


class SetConfigTests(unittest.TestCase):
def test_set_config(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
A ``_PyInterpreterConfig`` has been added and ``_Py_NewInterpreter()`` has
been renamed to ``_Py_NewInterpreterFromConfig()``. The
"isolated_subinterpreters" argument is now a granular config that captures
the previous behavior. Note that this is all "private" API.
3 changes: 1 addition & 2 deletions Modules/_posixsubprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -842,8 +842,7 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
}

PyInterpreterState *interp = PyInterpreterState_Get();
const PyConfig *config = _PyInterpreterState_GetConfig(interp);
if (config->_isolated_interpreter) {
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_SUBPROCESS)) {
PyErr_SetString(PyExc_RuntimeError,
"subprocess not supported for isolated subinterpreters");
return NULL;
Expand Down
63 changes: 63 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,66 @@ run_in_subinterp(PyObject *self, PyObject *args)
return PyLong_FromLong(r);
}

/* To run some code in a sub-interpreter. */
static PyObject *
run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
{
const char *code;
int allow_fork = -1;
int allow_subprocess = -1;
int allow_threads = -1;
int r;
PyThreadState *substate, *mainstate;
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
PyCompilerFlags cflags = {0};

static char *kwlist[] = {"code",
"allow_fork", "allow_subprocess", "allow_threads",
NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"s$ppp:run_in_subinterp_with_config", kwlist,
&code, &allow_fork, &allow_subprocess, &allow_threads)) {
return NULL;
}
if (allow_fork < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_fork");
return NULL;
}
if (allow_subprocess < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_subprocess");
return NULL;
}
if (allow_threads < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_threads");
return NULL;
}

mainstate = PyThreadState_Get();

PyThreadState_Swap(NULL);

const _PyInterpreterConfig config = {
.allow_fork = allow_fork,
.allow_subprocess = allow_subprocess,
.allow_threads = allow_threads,
};
substate = _Py_NewInterpreterFromConfig(&config);
if (substate == NULL) {
/* Since no new thread state was created, there is no exception to
propagate; raise a fresh one after swapping in the old thread
state. */
PyThreadState_Swap(mainstate);
PyErr_SetString(PyExc_RuntimeError, "sub-interpreter creation failed");
return NULL;
}
r = PyRun_SimpleStringFlags(code, &cflags);
Py_EndInterpreter(substate);

PyThreadState_Swap(mainstate);

return PyLong_FromLong(r);
}

static int
check_time_rounding(int round)
{
Expand Down Expand Up @@ -5842,6 +5902,9 @@ static PyMethodDef TestMethods[] = {
METH_NOARGS},
{"crash_no_current_thread", crash_no_current_thread, METH_NOARGS},
{"run_in_subinterp", run_in_subinterp, METH_VARARGS},
{"run_in_subinterp_with_config",
_PyCFunction_CAST(run_in_subinterp_with_config),
METH_VARARGS | METH_KEYWORDS},
{"pytime_object_to_time_t", test_pytime_object_to_time_t, METH_VARARGS},
{"pytime_object_to_timeval", test_pytime_object_to_timeval, METH_VARARGS},
{"pytime_object_to_timespec", test_pytime_object_to_timespec, METH_VARARGS},
Expand Down
46 changes: 46 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,51 @@ _testinternalcapi_optimize_cfg_impl(PyObject *module, PyObject *instructions,
}


static PyObject *
get_interp_settings(PyObject *self, PyObject *args)
{
int interpid = -1;
if (!PyArg_ParseTuple(args, "|i:get_interp_settings", &interpid)) {
return NULL;
}

PyInterpreterState *interp = NULL;
if (interpid < 0) {
PyThreadState *tstate = _PyThreadState_GET();
interp = tstate ? tstate->interp : _PyInterpreterState_Main();
}
else if (interpid == 0) {
interp = _PyInterpreterState_Main();
}
else {
PyErr_Format(PyExc_NotImplementedError,
"%zd", interpid);
return NULL;
}
assert(interp != NULL);

PyObject *settings = PyDict_New();
if (settings == NULL) {
return NULL;
}

/* Add the feature flags. */
PyObject *flags = PyLong_FromUnsignedLong(interp->feature_flags);
if (flags == NULL) {
Py_DECREF(settings);
return NULL;
}
int res = PyDict_SetItemString(settings, "feature_flags", flags);
Py_DECREF(flags);
if (res != 0) {
Py_DECREF(settings);
return NULL;
}

return settings;
}


static PyMethodDef TestMethods[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
Expand All @@ -569,6 +614,7 @@ static PyMethodDef TestMethods[] = {
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
{NULL, NULL} /* sentinel */
};

Expand Down
2 changes: 1 addition & 1 deletion Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,7 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
}

PyInterpreterState *interp = _PyInterpreterState_GET();
if (interp->config._isolated_interpreter) {
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) {
PyErr_SetString(PyExc_RuntimeError,
"thread is not supported for isolated subinterpreters");
return NULL;
Expand Down
3 changes: 1 addition & 2 deletions Modules/_winapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1090,8 +1090,7 @@ _winapi_CreateProcess_impl(PyObject *module,
}

PyInterpreterState *interp = PyInterpreterState_Get();
const PyConfig *config = _PyInterpreterState_GetConfig(interp);
if (config->_isolated_interpreter) {
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_SUBPROCESS)) {
PyErr_SetString(PyExc_RuntimeError,
"subprocess not supported for isolated subinterpreters");
return NULL;
Expand Down
Loading