gh-76785: Add PyInterpreterConfig Helpers (gh-117170)

These helpers make it easier to customize and inspect the config used to initialize interpreters.  This is especially valuable in our tests.  I found inspiration from the PyConfig API for the PyInterpreterConfig dict conversion stuff.  As part of this PR I've also added a bunch of tests.
This commit is contained in:
Eric Snow 2024-04-02 14:35:52 -06:00 committed by GitHub
parent cae4cdd07d
commit f341d6017d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 764 additions and 96 deletions

View File

@ -116,6 +116,22 @@ PyAPI_FUNC(char*) _Py_SetLocaleFromEnv(int category);
// Export for special main.c string compiling with source tracebacks
int _PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCompilerFlags *flags);
/* interpreter config */
// Export for _testinternalcapi shared extension
PyAPI_FUNC(int) _PyInterpreterConfig_InitFromState(
PyInterpreterConfig *,
PyInterpreterState *);
PyAPI_FUNC(PyObject *) _PyInterpreterConfig_AsDict(PyInterpreterConfig *);
PyAPI_FUNC(int) _PyInterpreterConfig_InitFromDict(
PyInterpreterConfig *,
PyObject *);
PyAPI_FUNC(int) _PyInterpreterConfig_UpdateFromDict(
PyInterpreterConfig *,
PyObject *);
#ifdef __cplusplus
}
#endif

View File

@ -1734,8 +1734,19 @@ def run_in_subinterp_with_config(code, *, own_gil=None, **config):
raise unittest.SkipTest("requires _testinternalcapi")
if own_gil is not None:
assert 'gil' not in config, (own_gil, config)
config['gil'] = 2 if own_gil else 1
return _testinternalcapi.run_in_subinterp_with_config(code, **config)
config['gil'] = 'own' if own_gil else 'shared'
else:
gil = config['gil']
if gil == 0:
config['gil'] = 'default'
elif gil == 1:
config['gil'] = 'shared'
elif gil == 2:
config['gil'] = 'own'
else:
raise NotImplementedError(gil)
config = types.SimpleNamespace(**config)
return _testinternalcapi.run_in_subinterp_with_config(code, config)
def _check_tracemalloc():

View File

@ -2204,6 +2204,257 @@ class SubinterpreterTest(unittest.TestCase):
self.assertEqual(main_attr_id, subinterp_attr_id)
class InterpreterConfigTests(unittest.TestCase):
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']
def iter_all_configs(self):
for use_main_obmalloc in (True, False):
for allow_fork in (True, False):
for allow_exec in (True, False):
for allow_threads in (True, False):
for allow_daemon in (True, False):
for checkext in (True, False):
for gil in ('shared', 'own', 'default'):
yield types.SimpleNamespace(
use_main_obmalloc=use_main_obmalloc,
allow_fork=allow_fork,
allow_exec=allow_exec,
allow_threads=allow_threads,
allow_daemon_threads=allow_daemon,
check_multi_interp_extensions=checkext,
gil=gil,
)
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))
def test_predefined_config(self):
def check(name, expected):
expected = self.supported[expected]
args = (name,) if name else ()
config1 = _testinternalcapi.new_interp_config(*args)
self.assert_ns_equal(config1, expected)
self.assertIsNot(config1, expected)
config2 = _testinternalcapi.new_interp_config(*args)
self.assert_ns_equal(config2, expected)
self.assertIsNot(config2, expected)
self.assertIsNot(config2, config1)
with self.subTest('default'):
check(None, 'isolated')
for name in self.supported:
with self.subTest(name):
check(name, name)
def test_update_from_dict(self):
for name, vanilla in self.supported.items():
with self.subTest(f'noop ({name})'):
expected = vanilla
overrides = vars(vanilla)
config = _testinternalcapi.new_interp_config(name, **overrides)
self.assert_ns_equal(config, expected)
with self.subTest(f'change all ({name})'):
overrides = {k: not v for k, v in vars(vanilla).items()}
for gil in self.gil_supported:
if vanilla.gil == gil:
continue
overrides['gil'] = gil
expected = types.SimpleNamespace(**overrides)
config = _testinternalcapi.new_interp_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 self.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 = _testinternalcapi.new_interp_config(
name, **overrides)
self.assert_ns_equal(config, expected)
with self.subTest('unsupported field'):
for name in self.supported:
with self.assertRaises(ValueError):
_testinternalcapi.new_interp_config(name, spam=True)
# Bad values for bool fields.
for field, value in vars(self.supported['empty']).items():
if field == 'gil':
continue
assert isinstance(value, bool)
for value in [1, '', 'spam', 1.0, None, object()]:
with self.subTest(f'unsupported value ({field}={value!r})'):
with self.assertRaises(TypeError):
_testinternalcapi.new_interp_config(**{field: value})
# Bad values for .gil.
for value in [True, 1, 1.0, None, object()]:
with self.subTest(f'unsupported value(gil={value!r})'):
with self.assertRaises(TypeError):
_testinternalcapi.new_interp_config(gil=value)
for value in ['', 'spam']:
with self.subTest(f'unsupported value (gil={value!r})'):
with self.assertRaises(ValueError):
_testinternalcapi.new_interp_config(gil=value)
@requires_subinterpreters
def test_interp_init(self):
questionable = [
# strange
dict(
allow_fork=True,
allow_exec=False,
),
dict(
gil='shared',
use_main_obmalloc=False,
),
# risky
dict(
allow_fork=True,
allow_threads=True,
),
# ought to be invalid?
dict(
allow_threads=False,
allow_daemon_threads=True,
),
dict(
gil='own',
use_main_obmalloc=True,
),
]
invalid = [
dict(
use_main_obmalloc=False,
check_multi_interp_extensions=False
),
]
def match(config, override_cases):
ns = vars(config)
for overrides in override_cases:
if dict(ns, **overrides) == ns:
return True
return False
def check(config):
script = 'pass'
rc = _testinternalcapi.run_in_subinterp_with_config(script, config)
self.assertEqual(rc, 0)
for config in self.iter_all_configs():
if config.gil == 'default':
continue
if match(config, invalid):
with self.subTest(f'invalid: {config}'):
with self.assertRaises(RuntimeError):
check(config)
elif match(config, questionable):
with self.subTest(f'questionable: {config}'):
check(config)
else:
with self.subTest(f'valid: {config}'):
check(config)
@requires_subinterpreters
def test_get_config(self):
@contextlib.contextmanager
def new_interp(config):
interpid = _testinternalcapi.new_interpreter(config)
try:
yield interpid
finally:
try:
_interpreters.destroy(interpid)
except _interpreters.InterpreterNotFoundError:
pass
with self.subTest('main'):
expected = _testinternalcapi.new_interp_config('legacy')
expected.gil = 'own'
interpid = _interpreters.get_main()
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, expected)
with self.subTest('isolated'):
expected = _testinternalcapi.new_interp_config('isolated')
with new_interp('isolated') as interpid:
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, expected)
with self.subTest('legacy'):
expected = _testinternalcapi.new_interp_config('legacy')
with new_interp('legacy') as interpid:
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, expected)
with self.subTest('custom'):
orig = _testinternalcapi.new_interp_config(
'empty',
use_main_obmalloc=True,
gil='shared',
)
with new_interp(orig) as interpid:
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, orig)
@requires_subinterpreters
class InterpreterIDTests(unittest.TestCase):

View File

@ -1823,15 +1823,19 @@ class SubinterpImportTests(unittest.TestCase):
**(self.ISOLATED if isolated else self.NOT_ISOLATED),
check_multi_interp_extensions=strict,
)
gil = kwargs['gil']
kwargs['gil'] = 'default' if gil == 0 else (
'shared' if gil == 1 else 'own' if gil == 2 else gil)
_, out, err = script_helper.assert_python_ok('-c', textwrap.dedent(f'''
import _testinternalcapi, sys
assert (
{name!r} in sys.builtin_module_names or
{name!r} not in sys.modules
), repr({name!r})
config = type(sys.implementation)(**{kwargs})
ret = _testinternalcapi.run_in_subinterp_with_config(
{self.import_script(name, "sys.stdout.fileno()")!r},
**{kwargs},
config,
)
assert ret == 0, ret
'''))
@ -1847,12 +1851,16 @@ class SubinterpImportTests(unittest.TestCase):
**(self.ISOLATED if isolated else self.NOT_ISOLATED),
check_multi_interp_extensions=True,
)
gil = kwargs['gil']
kwargs['gil'] = 'default' if gil == 0 else (
'shared' if gil == 1 else 'own' if gil == 2 else gil)
_, out, err = script_helper.assert_python_ok('-c', textwrap.dedent(f'''
import _testinternalcapi, sys
assert {name!r} not in sys.modules, {name!r}
config = type(sys.implementation)(**{kwargs})
ret = _testinternalcapi.run_in_subinterp_with_config(
{self.import_script(name, "sys.stdout.fileno()")!r},
**{kwargs},
config,
)
assert ret == 0, ret
'''))

View File

@ -440,6 +440,7 @@ PYTHON_OBJS= \
Python/import.o \
Python/importdl.o \
Python/initconfig.o \
Python/interpconfig.o \
Python/instrumentation.o \
Python/intrinsics.o \
Python/jit.o \
@ -1687,6 +1688,10 @@ Modules/_xxinterpchannelsmodule.o: $(srcdir)/Modules/_xxinterpchannelsmodule.c $
Python/crossinterp.o: $(srcdir)/Python/crossinterp.c $(srcdir)/Python/crossinterp_data_lookup.h $(srcdir)/Python/crossinterp_exceptions.h
Python/initconfig.o: $(srcdir)/Python/initconfig.c $(srcdir)/Python/config_common.h
Python/interpconfig.o: $(srcdir)/Python/interpconfig.c $(srcdir)/Python/config_common.h
Python/dynload_shlib.o: $(srcdir)/Python/dynload_shlib.c Makefile
$(CC) -c $(PY_CORE_CFLAGS) \
-DSOABI='"$(SOABI)"' \

View File

@ -23,10 +23,12 @@
#include "pycore_initconfig.h" // _Py_GetConfigsAsDict()
#include "pycore_interp.h" // _PyInterpreterState_GetConfigCopy()
#include "pycore_long.h" // _PyLong_Sign()
#include "pycore_namespace.h" // _PyNamespace_New()
#include "pycore_object.h" // _PyObject_IsFreed()
#include "pycore_optimizer.h" // _Py_UopsSymbol, etc.
#include "pycore_pathconfig.h" // _PyPathConfig_ClearGlobal()
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
#include "pycore_pylifecycle.h" // _PyInterpreterConfig_AsDict()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "clinic/_testinternalcapi.c.h"
@ -1355,83 +1357,153 @@ dict_getitem_knownhash(PyObject *self, PyObject *args)
}
static int
init_named_interp_config(PyInterpreterConfig *config, const char *name)
{
if (name == NULL) {
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 PyObject *
new_interp_config(PyObject *self, PyObject *args, PyObject *kwds)
{
const char *name = NULL;
if (!PyArg_ParseTuple(args, "|s:new_config", &name)) {
return NULL;
}
PyObject *overrides = kwds;
if (name == NULL) {
name = "isolated";
}
PyInterpreterConfig config;
if (init_named_interp_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;
}
static PyObject *
get_interp_config(PyObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"id", NULL};
PyObject *idobj = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds,
"O:get_config", kwlist, &idobj))
{
return NULL;
}
PyInterpreterState *interp;
if (idobj == NULL) {
interp = PyInterpreterState_Get();
}
else {
interp = _PyInterpreterState_LookUpIDObject(idobj);
if (interp == NULL) {
return NULL;
}
}
PyInterpreterConfig config;
if (_PyInterpreterConfig_InitFromState(&config, interp) < 0) {
return NULL;
}
PyObject *dict = _PyInterpreterConfig_AsDict(&config);
if (dict == NULL) {
return NULL;
}
PyObject *configobj = _PyNamespace_New(dict);
Py_DECREF(dict);
return configobj;
}
static int
interp_config_from_object(PyObject *configobj, PyInterpreterConfig *config)
{
if (configobj == NULL || configobj == Py_None) {
if (init_named_interp_config(config, NULL) < 0) {
return -1;
}
}
else if (PyUnicode_Check(configobj)) {
if (init_named_interp_config(config, PyUnicode_AsUTF8(configobj)) < 0) {
return -1;
}
}
else {
PyObject *dict = PyObject_GetAttrString(configobj, "__dict__");
if (dict == NULL) {
PyErr_Format(PyExc_TypeError, "bad config %R", configobj);
return -1;
}
int res = _PyInterpreterConfig_InitFromDict(config, dict);
Py_DECREF(dict);
if (res < 0) {
return -1;
}
}
return 0;
}
/* 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 use_main_obmalloc = -1;
int allow_fork = -1;
int allow_exec = -1;
int allow_threads = -1;
int allow_daemon_threads = -1;
int check_multi_interp_extensions = -1;
int gil = -1;
int r;
PyThreadState *substate, *mainstate;
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
PyCompilerFlags cflags = {0};
static char *kwlist[] = {"code",
"use_main_obmalloc",
"allow_fork",
"allow_exec",
"allow_threads",
"allow_daemon_threads",
"check_multi_interp_extensions",
"gil",
NULL};
PyObject *configobj;
static char *kwlist[] = {"code", "config", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"s$ppppppi:run_in_subinterp_with_config", kwlist,
&code, &use_main_obmalloc,
&allow_fork, &allow_exec,
&allow_threads, &allow_daemon_threads,
&check_multi_interp_extensions,
&gil)) {
return NULL;
}
if (use_main_obmalloc < 0) {
PyErr_SetString(PyExc_ValueError, "missing use_main_obmalloc");
return NULL;
}
if (allow_fork < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_fork");
return NULL;
}
if (allow_exec < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_exec");
return NULL;
}
if (allow_threads < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_threads");
return NULL;
}
if (gil < 0) {
PyErr_SetString(PyExc_ValueError, "missing gil");
return NULL;
}
if (allow_daemon_threads < 0) {
PyErr_SetString(PyExc_ValueError, "missing allow_daemon_threads");
return NULL;
}
if (check_multi_interp_extensions < 0) {
PyErr_SetString(PyExc_ValueError, "missing check_multi_interp_extensions");
"sO:run_in_subinterp_with_config", kwlist,
&code, &configobj))
{
return NULL;
}
mainstate = PyThreadState_Get();
PyInterpreterConfig config;
if (interp_config_from_object(configobj, &config) < 0) {
return NULL;
}
PyThreadState *mainstate = PyThreadState_Get();
PyThreadState_Swap(NULL);
const PyInterpreterConfig config = {
.use_main_obmalloc = use_main_obmalloc,
.allow_fork = allow_fork,
.allow_exec = allow_exec,
.allow_threads = allow_threads,
.allow_daemon_threads = allow_daemon_threads,
.check_multi_interp_extensions = check_multi_interp_extensions,
.gil = gil,
};
PyThreadState *substate;
PyStatus status = Py_NewInterpreterFromConfig(&substate, &config);
if (PyStatus_Exception(status)) {
/* Since no new thread state was created, there is no exception to
@ -1445,7 +1517,9 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
return NULL;
}
assert(substate != NULL);
r = PyRun_SimpleStringFlags(code, &cflags);
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
PyCompilerFlags cflags = {0};
int r = PyRun_SimpleStringFlags(code, &cflags);
Py_EndInterpreter(substate);
PyThreadState_Swap(mainstate);
@ -1473,13 +1547,21 @@ unused_interpreter_id(PyObject *self, PyObject *Py_UNUSED(ignored))
}
static PyObject *
new_interpreter(PyObject *self, PyObject *Py_UNUSED(ignored))
new_interpreter(PyObject *self, PyObject *args)
{
PyObject *configobj = NULL;
if (!PyArg_ParseTuple(args, "|O:new_interpreter", &configobj)) {
return NULL;
}
PyInterpreterConfig config;
if (interp_config_from_object(configobj, &config) < 0) {
return NULL;
}
// Unlike _interpreters.create(), we do not automatically link
// the interpreter to its refcount.
PyThreadState *save_tstate = PyThreadState_Get();
const PyInterpreterConfig config = \
(PyInterpreterConfig)_PyInterpreterConfig_INIT;
PyThreadState *tstate = NULL;
PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config);
PyThreadState_Swap(save_tstate);
@ -1846,12 +1928,16 @@ static PyMethodDef module_functions[] = {
{"get_object_dict_values", get_object_dict_values, METH_O},
{"hamt", new_hamt, METH_NOARGS},
{"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS},
{"new_interp_config", _PyCFunction_CAST(new_interp_config),
METH_VARARGS | METH_KEYWORDS},
{"get_interp_config", _PyCFunction_CAST(get_interp_config),
METH_VARARGS | METH_KEYWORDS},
{"run_in_subinterp_with_config",
_PyCFunction_CAST(run_in_subinterp_with_config),
METH_VARARGS | METH_KEYWORDS},
{"normalize_interp_id", normalize_interp_id, METH_O},
{"unused_interpreter_id", unused_interpreter_id, METH_NOARGS},
{"new_interpreter", new_interpreter, METH_NOARGS},
{"new_interpreter", new_interpreter, METH_VARARGS},
{"interpreter_exists", interpreter_exists, METH_O},
{"get_interpreter_refcount", get_interpreter_refcount, METH_O},
{"link_interpreter_refcount", link_interpreter_refcount, METH_O},

View File

@ -222,6 +222,7 @@
<ClCompile Include="..\Python\import.c" />
<ClCompile Include="..\Python\importdl.c" />
<ClCompile Include="..\Python\initconfig.c" />
<ClCompile Include="..\Python\interpconfig.c" />
<ClCompile Include="..\Python\intrinsics.c" />
<ClCompile Include="..\Python\instrumentation.c" />
<ClCompile Include="..\Python\jit.c" />

View File

@ -229,6 +229,9 @@
<ClCompile Include="..\Python\initconfig.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Python\interpconfig.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Python\intrinsics.c">
<Filter>Source Files</Filter>
</ClCompile>

View File

@ -587,6 +587,7 @@
<ClCompile Include="..\Python\import.c" />
<ClCompile Include="..\Python\importdl.c" />
<ClCompile Include="..\Python\initconfig.c" />
<ClCompile Include="..\Python\interpconfig.c" />
<ClCompile Include="..\Python\intrinsics.c" />
<ClCompile Include="..\Python\instrumentation.c" />
<ClCompile Include="..\Python\jit.c" />

View File

@ -1343,6 +1343,9 @@
<ClCompile Include="..\Python\initconfig.c">
<Filter>Python</Filter>
</ClCompile>
<ClCompile Include="..\Python\interpconfig.c">
<Filter>Python</Filter>
</ClCompile>
<ClCompile Include="..\Python\intrinsics.c">
<Filter>Source Files</Filter>
</ClCompile>

36
Python/config_common.h Normal file
View File

@ -0,0 +1,36 @@
static inline int
_config_dict_get(PyObject *dict, const char *name, PyObject **p_item)
{
PyObject *item;
if (PyDict_GetItemStringRef(dict, name, &item) < 0) {
return -1;
}
if (item == NULL) {
// We do not set an exception.
return -1;
}
*p_item = item;
return 0;
}
static PyObject*
config_dict_get(PyObject *dict, const char *name)
{
PyObject *item;
if (_config_dict_get(dict, name, &item) < 0) {
if (!PyErr_Occurred()) {
PyErr_Format(PyExc_ValueError, "missing config key: %s", name);
}
return NULL;
}
return item;
}
static void
config_dict_invalid_type(const char *name)
{
PyErr_Format(PyExc_TypeError, "invalid config type: %s", name);
}

View File

@ -24,6 +24,9 @@
# endif
#endif
#include "config_common.h"
/* --- PyConfig spec ---------------------------------------------- */
typedef enum {
@ -1098,21 +1101,6 @@ _PyConfig_AsDict(const PyConfig *config)
}
static PyObject*
config_dict_get(PyObject *dict, const char *name)
{
PyObject *item;
if (PyDict_GetItemStringRef(dict, name, &item) < 0) {
return NULL;
}
if (item == NULL) {
PyErr_Format(PyExc_ValueError, "missing config key: %s", name);
return NULL;
}
return item;
}
static void
config_dict_invalid_value(const char *name)
{
@ -1120,13 +1108,6 @@ config_dict_invalid_value(const char *name)
}
static void
config_dict_invalid_type(const char *name)
{
PyErr_Format(PyExc_TypeError, "invalid config type: %s", name);
}
static int
config_dict_get_int(PyObject *dict, const char *name, int *result)
{

266
Python/interpconfig.c Normal file
View File

@ -0,0 +1,266 @@
/* PyInterpreterConfig API */
#include "Python.h"
#include "pycore_pylifecycle.h"
#include <stdbool.h>
#include "config_common.h"
static const char *
gil_flag_to_str(int flag)
{
switch (flag) {
case PyInterpreterConfig_DEFAULT_GIL:
return "default";
case PyInterpreterConfig_SHARED_GIL:
return "shared";
case PyInterpreterConfig_OWN_GIL:
return "own";
default:
PyErr_SetString(PyExc_SystemError,
"invalid interpreter config 'gil' value");
return NULL;
}
}
static int
gil_flag_from_str(const char *str, int *p_flag)
{
int flag;
if (str == NULL) {
flag = PyInterpreterConfig_DEFAULT_GIL;
}
else if (strcmp(str, "default") == 0) {
flag = PyInterpreterConfig_DEFAULT_GIL;
}
else if (strcmp(str, "shared") == 0) {
flag = PyInterpreterConfig_SHARED_GIL;
}
else if (strcmp(str, "own") == 0) {
flag = PyInterpreterConfig_OWN_GIL;
}
else {
PyErr_Format(PyExc_ValueError,
"unsupported interpreter config .gil value '%s'", str);
return -1;
}
*p_flag = flag;
return 0;
}
PyObject *
_PyInterpreterConfig_AsDict(PyInterpreterConfig *config)
{
PyObject *dict = PyDict_New();
if (dict == NULL) {
return NULL;
}
#define ADD(NAME, OBJ) \
do { \
int res = PyDict_SetItemString(dict, NAME, (OBJ)); \
Py_DECREF(OBJ); \
if (res < 0) { \
goto error; \
} \
} while (0)
#define ADD_BOOL(FIELD) \
ADD(#FIELD, Py_NewRef(config->FIELD ? Py_True : Py_False))
#define ADD_STR(FIELD, STR) \
do { \
if (STR == NULL) { \
goto error; \
} \
PyObject *obj = PyUnicode_FromString(STR); \
if (obj == NULL) { \
goto error; \
} \
ADD(#FIELD, obj); \
} while (0)
ADD_BOOL(use_main_obmalloc);
ADD_BOOL(allow_fork);
ADD_BOOL(allow_exec);
ADD_BOOL(allow_threads);
ADD_BOOL(allow_daemon_threads);
ADD_BOOL(check_multi_interp_extensions);
ADD_STR(gil, gil_flag_to_str(config->gil));
#undef ADD_STR
#undef ADD_BOOL
#undef ADD
return dict;
error:
Py_DECREF(dict);
return NULL;
}
static int
_config_dict_get_bool(PyObject *dict, const char *name, int *p_flag)
{
PyObject *item;
if (_config_dict_get(dict, name, &item) < 0) {
return -1;
}
// For now we keep things strict, rather than using PyObject_IsTrue().
int flag = item == Py_True;
if (!flag && item != Py_False) {
Py_DECREF(item);
config_dict_invalid_type(name);
return -1;
}
Py_DECREF(item);
*p_flag = flag;
return 0;
}
static int
_config_dict_copy_str(PyObject *dict, const char *name,
char *buf, size_t bufsize)
{
PyObject *item;
if (_config_dict_get(dict, name, &item) < 0) {
return -1;
}
if (!PyUnicode_Check(item)) {
Py_DECREF(item);
config_dict_invalid_type(name);
return -1;
}
strncpy(buf, PyUnicode_AsUTF8(item), bufsize-1);
buf[bufsize-1] = '\0';
Py_DECREF(item);
return 0;
}
static int
interp_config_from_dict(PyObject *origdict, PyInterpreterConfig *config,
bool missing_allowed)
{
PyObject *dict = PyDict_New();
if (dict == NULL) {
return -1;
}
if (PyDict_Update(dict, origdict) < 0) {
goto error;
}
#define CHECK(NAME) \
do { \
if (PyErr_Occurred()) { \
goto error; \
} \
else { \
if (!missing_allowed) { \
(void)config_dict_get(dict, NAME); \
assert(PyErr_Occurred()); \
goto error; \
} \
} \
} while (0)
#define COPY_BOOL(FIELD) \
do { \
int flag; \
if (_config_dict_get_bool(dict, #FIELD, &flag) < 0) { \
CHECK(#FIELD); \
} \
else { \
config->FIELD = flag; \
(void)PyDict_PopString(dict, #FIELD, NULL); \
} \
} while (0)
COPY_BOOL(use_main_obmalloc);
COPY_BOOL(allow_fork);
COPY_BOOL(allow_exec);
COPY_BOOL(allow_threads);
COPY_BOOL(allow_daemon_threads);
COPY_BOOL(check_multi_interp_extensions);
// PyInterpreterConfig.gil
char buf[20];
if (_config_dict_copy_str(dict, "gil", buf, 20) < 0) {
CHECK("gil");
}
else {
int flag;
if (gil_flag_from_str(buf, &flag) < 0) {
goto error;
}
config->gil = flag;
(void)PyDict_PopString(dict, "gil", NULL);
}
#undef COPY_BOOL
#undef CHECK
Py_ssize_t unused = PyDict_GET_SIZE(dict);
if (unused == 1) {
PyErr_Format(PyExc_ValueError,
"config dict has 1 extra item (%R)", dict);
goto error;
}
else if (unused > 0) {
PyErr_Format(PyExc_ValueError,
"config dict has %d extra items (%R)", unused, dict);
goto error;
}
return 0;
error:
Py_DECREF(dict);
return -1;
}
int
_PyInterpreterConfig_InitFromDict(PyInterpreterConfig *config, PyObject *dict)
{
if (!PyDict_Check(dict)) {
PyErr_SetString(PyExc_TypeError, "dict expected");
return -1;
}
if (interp_config_from_dict(dict, config, false) < 0) {
return -1;
}
return 0;
}
int
_PyInterpreterConfig_UpdateFromDict(PyInterpreterConfig *config, PyObject *dict)
{
if (!PyDict_Check(dict)) {
PyErr_SetString(PyExc_TypeError, "dict expected");
return -1;
}
if (interp_config_from_dict(dict, config, true) < 0) {
return -1;
}
return 0;
}
int
_PyInterpreterConfig_InitFromState(PyInterpreterConfig *config,
PyInterpreterState *interp)
{
// Populate the config by re-constructing the values from the interpreter.
*config = (PyInterpreterConfig){
#define FLAG(flag) \
(interp->feature_flags & Py_RTFLAGS_ ## flag)
.use_main_obmalloc = FLAG(USE_MAIN_OBMALLOC),
.allow_fork = FLAG(FORK),
.allow_exec = FLAG(EXEC),
.allow_threads = FLAG(THREADS),
.allow_daemon_threads = FLAG(DAEMON_THREADS),
.check_multi_interp_extensions = FLAG(MULTI_INTERP_EXTENSIONS),
#undef FLAG
.gil = interp->ceval.own_gil
? PyInterpreterConfig_OWN_GIL
: PyInterpreterConfig_SHARED_GIL,
};
return 0;
}