]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-107954, PEP 741: Add PyConfig_Get()/Set() functions (#123472)
authorVictor Stinner <vstinner@python.org>
Mon, 2 Sep 2024 21:25:08 +0000 (23:25 +0200)
committerGitHub <noreply@github.com>
Mon, 2 Sep 2024 21:25:08 +0000 (23:25 +0200)
Add PyConfig_Get(), PyConfig_GetInt(), PyConfig_Set() and
PyConfig_Names() functions to get and set the current runtime Python
configuration.

Add visibility and "sys spec" to config and preconfig specifications.

_PyConfig_AsDict() now converts PyConfig.xoptions as a dictionary.

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
19 files changed:
Doc/c-api/init_config.rst
Doc/whatsnew/3.14.rst
Include/cpython/initconfig.h
Include/internal/pycore_initconfig.h
Include/internal/pycore_sysmodule.h
Lib/test/_test_embed_set_config.py
Lib/test/test_capi/test_config.py [new file with mode: 0644]
Lib/test/test_embed.py
Misc/NEWS.d/next/C_API/2024-08-29-15-55-55.gh-issue-107954.pr2O50.rst [new file with mode: 0644]
Modules/Setup.stdlib.in
Modules/_testcapi/config.c [new file with mode: 0644]
Modules/_testcapi/parts.h
Modules/_testcapimodule.c
PCbuild/_testcapi.vcxproj
PCbuild/_testcapi.vcxproj.filters
Python/initconfig.c
Python/sysmodule.c
Tools/c-analyzer/cpython/_parser.py
Tools/c-analyzer/cpython/ignored.tsv

index 6e2e04fba55a45a999b9fe76cc5b97d19cde72bd..355b8e010e0f96234ecae25e72f6373e1275f295 100644 (file)
@@ -1605,6 +1605,75 @@ customized Python always running in isolated mode using
 :c:func:`Py_RunMain`.
 
 
+Runtime Python configuration API
+================================
+
+The configuration option *name* parameter must be a non-NULL null-terminated
+UTF-8 encoded string.
+
+Some options are read from the :mod:`sys` attributes. For example, the option
+``"argv"`` is read from :data:`sys.argv`.
+
+
+.. c:function:: PyObject* PyConfig_Get(const char *name)
+
+   Get the current runtime value of a configuration option as a Python object.
+
+   * Return a new reference on success.
+   * Set an exception and return ``NULL`` on error.
+
+   The object type depends on the configuration option. It can be:
+
+   * ``bool``
+   * ``int``
+   * ``str``
+   * ``list[str]``
+   * ``dict[str, str]``
+
+   The caller must hold the GIL. The function cannot be called before
+   Python initialization nor after Python finalization.
+
+   .. versionadded:: 3.14
+
+
+.. c:function:: int PyConfig_GetInt(const char *name, int *value)
+
+   Similar to :c:func:`PyConfig_Get`, but get the value as a C int.
+
+   * Return ``0`` on success.
+   * Set an exception and return ``-1`` on error.
+
+   .. versionadded:: 3.14
+
+
+.. c:function:: PyObject* PyConfig_Names(void)
+
+   Get all configuration option names as a ``frozenset``.
+
+   * Return a new reference on success.
+   * Set an exception and return ``NULL`` on error.
+
+   The caller must hold the GIL. The function cannot be called before
+   Python initialization nor after Python finalization.
+
+   .. versionadded:: 3.14
+
+
+.. c:function:: int PyConfig_Set(const char *name, PyObject *value)
+
+   Set the current runtime value of a configuration option.
+
+   * Raise a :exc:`ValueError` if there is no option *name*.
+   * Raise a :exc:`ValueError` if *value* is an invalid value.
+   * Raise a :exc:`ValueError` if the option is read-only (cannot be set).
+   * Raise a :exc:`TypeError` if *value* has not the proper type.
+
+   The caller must hold the GIL. The function cannot be called before
+   Python initialization nor after Python finalization.
+
+   .. versionadded:: 3.14
+
+
 Py_GetArgcArgv()
 ================
 
index 2ab4102f32ab0b227f2f8a9a6830a47462ccfade..4817c5258052e1168b13cd4b6f7a2e5c39f30123 100644 (file)
@@ -492,6 +492,15 @@ New Features
 * Add :c:func:`Py_HashBuffer` to compute and return the hash value of a buffer.
   (Contributed by Antoine Pitrou and Victor Stinner in :gh:`122854`.)
 
+* Add functions to get and set the current runtime Python configuration:
+
+  * :c:func:`PyConfig_Get`
+  * :c:func:`PyConfig_GetInt`
+  * :c:func:`PyConfig_Set`
+  * :c:func:`PyConfig_Names`
+
+  (Contributed by Victor Stinner in :gh:`107954`.)
+
 
 Porting to Python 3.14
 ----------------------
index 5da5ef9e5431b1f79bb5134a35f79ce95d90fea6..83faab7e27059536474efab72eb3a78fe7ab59dd 100644 (file)
@@ -260,6 +260,14 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config,
     Py_ssize_t length, wchar_t **items);
 
 
+/* --- PyConfig_Get() ----------------------------------------- */
+
+PyAPI_FUNC(PyObject*) PyConfig_Get(const char *name);
+PyAPI_FUNC(int) PyConfig_GetInt(const char *name, int *value);
+PyAPI_FUNC(PyObject*) PyConfig_Names(void);
+PyAPI_FUNC(int) PyConfig_Set(const char *name, PyObject *value);
+
+
 /* --- Helper functions --------------------------------------- */
 
 /* Get the original command line arguments, before Python modified them.
index 6bf1b53bffd3ba39630d449b4887df6fef487d63..25ec8cec3bd249deebc3c3af59292c695042c766 100644 (file)
@@ -181,7 +181,7 @@ extern PyStatus _PyConfig_Write(const PyConfig *config,
 extern PyStatus _PyConfig_SetPyArgv(
     PyConfig *config,
     const _PyArgv *args);
-
+extern PyObject* _PyConfig_CreateXOptionsDict(const PyConfig *config);
 
 extern void _Py_DumpPathConfig(PyThreadState *tstate);
 
index 9b8eafd3d6cfd3f467709ff338466895a460bc23..a1d795e284f6ac2a33d86ee455d4f0ffc345f92c 100644 (file)
@@ -29,6 +29,9 @@ extern int _PySys_SetAttr(PyObject *, PyObject *);
 extern int _PySys_ClearAttrString(PyInterpreterState *interp,
                                   const char *name, int verbose);
 
+extern int _PySys_SetFlagObj(Py_ssize_t pos, PyObject *new_value);
+extern int _PySys_SetIntMaxStrDigits(int maxdigits);
+
 #ifdef __cplusplus
 }
 #endif
index 23423d5b7a583dd81da7fa263a100b6d5f4c63eb..7edb35da463aa07d9a22739998b2d46f795bd5a6 100644 (file)
@@ -137,7 +137,8 @@ class SetConfigTests(unittest.TestCase):
             'warnoptions',
             'module_search_paths',
         ):
-            value_tests.append((key, invalid_wstrlist))
+            if key != 'xoptions':
+                value_tests.append((key, invalid_wstrlist))
             type_tests.append((key, 123))
             type_tests.append((key, "abc"))
             type_tests.append((key, [123]))
@@ -160,14 +161,14 @@ class SetConfigTests(unittest.TestCase):
     def test_flags(self):
         bool_options = set(BOOL_OPTIONS)
         for sys_attr, key, value in (
-            ("debug", "parser_debug", 1),
-            ("inspect", "inspect", 2),
-            ("interactive", "interactive", 3),
-            ("optimize", "optimization_level", 4),
-            ("verbose", "verbose", 1),
-            ("bytes_warning", "bytes_warning", 10),
-            ("quiet", "quiet", 11),
-            ("isolated", "isolated", 12),
+            ("debug", "parser_debug", 2),
+            ("inspect", "inspect", 3),
+            ("interactive", "interactive", 4),
+            ("optimize", "optimization_level", 5),
+            ("verbose", "verbose", 6),
+            ("bytes_warning", "bytes_warning", 7),
+            ("quiet", "quiet", 8),
+            ("isolated", "isolated", 9),
         ):
             with self.subTest(sys=sys_attr, key=key, value=value):
                 self.set_config(**{key: value, 'parse_argv': 0})
@@ -228,9 +229,9 @@ class SetConfigTests(unittest.TestCase):
         self.check(warnoptions=[])
         self.check(warnoptions=["default", "ignore"])
 
-        self.set_config(xoptions=[])
+        self.set_config(xoptions={})
         self.assertEqual(sys._xoptions, {})
-        self.set_config(xoptions=["dev", "tracemalloc=5"])
+        self.set_config(xoptions={"dev": True, "tracemalloc": "5"})
         self.assertEqual(sys._xoptions, {"dev": True, "tracemalloc": "5"})
 
     def test_pathconfig(self):
diff --git a/Lib/test/test_capi/test_config.py b/Lib/test/test_capi/test_config.py
new file mode 100644 (file)
index 0000000..01637e1
--- /dev/null
@@ -0,0 +1,379 @@
+"""
+Tests PyConfig_Get() and PyConfig_Set() C API (PEP 741).
+"""
+import os
+import sys
+import sysconfig
+import types
+import unittest
+from test import support
+from test.support import import_helper
+
+_testcapi = import_helper.import_module('_testcapi')
+
+
+# Is the Py_STATS macro defined?
+Py_STATS = hasattr(sys, '_stats_on')
+
+
+class CAPITests(unittest.TestCase):
+    def test_config_get(self):
+        # Test PyConfig_Get()
+        config_get = _testcapi.config_get
+        config_names = _testcapi.config_names
+
+        TEST_VALUE = {
+            str: "TEST_MARKER_STR",
+            str | None: "TEST_MARKER_OPT_STR",
+            list[str]: ("TEST_MARKER_STR_TUPLE",),
+            dict[str, str | bool]: {"x": "value", "y": True},
+        }
+
+        # read config options and check their type
+        options = [
+            ("allocator", int, None),
+            ("argv", list[str], "argv"),
+            ("base_exec_prefix", str | None, "base_exec_prefix"),
+            ("base_executable", str | None, "_base_executable"),
+            ("base_prefix", str | None, "base_prefix"),
+            ("buffered_stdio", bool, None),
+            ("bytes_warning", int, None),
+            ("check_hash_pycs_mode", str, None),
+            ("code_debug_ranges", bool, None),
+            ("configure_c_stdio", bool, None),
+            ("coerce_c_locale", bool, None),
+            ("coerce_c_locale_warn", bool, None),
+            ("configure_locale", bool, None),
+            ("cpu_count", int, None),
+            ("dev_mode", bool, None),
+            ("dump_refs", bool, None),
+            ("dump_refs_file", str | None, None),
+            ("exec_prefix", str | None, "exec_prefix"),
+            ("executable", str | None, "executable"),
+            ("faulthandler", bool, None),
+            ("filesystem_encoding", str, None),
+            ("filesystem_errors", str, None),
+            ("hash_seed", int, None),
+            ("home", str | None, None),
+            ("import_time", bool, None),
+            ("inspect", bool, None),
+            ("install_signal_handlers", bool, None),
+            ("int_max_str_digits", int, None),
+            ("interactive", bool, None),
+            ("isolated", bool, None),
+            ("malloc_stats", bool, None),
+            ("module_search_paths", list[str], "path"),
+            ("optimization_level", int, None),
+            ("orig_argv", list[str], "orig_argv"),
+            ("parser_debug", bool, None),
+            ("parse_argv", bool, None),
+            ("pathconfig_warnings", bool, None),
+            ("perf_profiling", bool, None),
+            ("platlibdir", str, "platlibdir"),
+            ("prefix", str | None, "prefix"),
+            ("program_name", str, None),
+            ("pycache_prefix", str | None, "pycache_prefix"),
+            ("quiet", bool, None),
+            ("run_command", str | None, None),
+            ("run_filename", str | None, None),
+            ("run_module", str | None, None),
+            ("safe_path", bool, None),
+            ("show_ref_count", bool, None),
+            ("site_import", bool, None),
+            ("skip_source_first_line", bool, None),
+            ("stdio_encoding", str, None),
+            ("stdio_errors", str, None),
+            ("stdlib_dir", str | None, "_stdlib_dir"),
+            ("tracemalloc", int, None),
+            ("use_environment", bool, None),
+            ("use_frozen_modules", bool, None),
+            ("use_hash_seed", bool, None),
+            ("user_site_directory", bool, None),
+            ("utf8_mode", bool, None),
+            ("verbose", int, None),
+            ("warn_default_encoding", bool, None),
+            ("warnoptions", list[str], "warnoptions"),
+            ("write_bytecode", bool, None),
+            ("xoptions", dict[str, str | bool], "_xoptions"),
+        ]
+        if support.Py_DEBUG:
+            options.append(("run_presite", str | None, None))
+        if sysconfig.get_config_var('Py_GIL_DISABLED'):
+            options.append(("enable_gil", int, None))
+        if support.MS_WINDOWS:
+            options.extend((
+                ("legacy_windows_stdio", bool, None),
+                ("legacy_windows_fs_encoding", bool, None),
+            ))
+        if Py_STATS:
+            options.extend((
+                ("_pystats", bool, None),
+            ))
+
+        for name, option_type, sys_attr in options:
+            with self.subTest(name=name, option_type=option_type,
+                              sys_attr=sys_attr):
+                value = config_get(name)
+                if isinstance(option_type, types.GenericAlias):
+                    self.assertIsInstance(value, option_type.__origin__)
+                    if option_type.__origin__ == dict:
+                        key_type = option_type.__args__[0]
+                        value_type = option_type.__args__[1]
+                        for item in value.items():
+                            self.assertIsInstance(item[0], key_type)
+                            self.assertIsInstance(item[1], value_type)
+                    else:
+                        item_type = option_type.__args__[0]
+                        for item in value:
+                            self.assertIsInstance(item, item_type)
+                else:
+                    self.assertIsInstance(value, option_type)
+
+                if sys_attr is not None:
+                    expected = getattr(sys, sys_attr)
+                    self.assertEqual(expected, value)
+
+                    override = TEST_VALUE[option_type]
+                    with support.swap_attr(sys, sys_attr, override):
+                        self.assertEqual(config_get(name), override)
+
+        # check that the test checks all options
+        self.assertEqual(sorted(name for name, option_type, sys_attr in options),
+                         sorted(config_names()))
+
+    def test_config_get_sys_flags(self):
+        # Test PyConfig_Get()
+        config_get = _testcapi.config_get
+
+        # compare config options with sys.flags
+        for flag, name, negate in (
+            ("debug", "parser_debug", False),
+            ("inspect", "inspect", False),
+            ("interactive", "interactive", False),
+            ("optimize", "optimization_level", False),
+            ("dont_write_bytecode", "write_bytecode", True),
+            ("no_user_site", "user_site_directory", True),
+            ("no_site", "site_import", True),
+            ("ignore_environment", "use_environment", True),
+            ("verbose", "verbose", False),
+            ("bytes_warning", "bytes_warning", False),
+            ("quiet", "quiet", False),
+            # "hash_randomization" is tested below
+            ("isolated", "isolated", False),
+            ("dev_mode", "dev_mode", False),
+            ("utf8_mode", "utf8_mode", False),
+            ("warn_default_encoding", "warn_default_encoding", False),
+            ("safe_path", "safe_path", False),
+            ("int_max_str_digits", "int_max_str_digits", False),
+            # "gil" is tested below
+        ):
+            with self.subTest(flag=flag, name=name, negate=negate):
+                value = config_get(name)
+                if negate:
+                    value = not value
+                self.assertEqual(getattr(sys.flags, flag), value)
+
+        self.assertEqual(sys.flags.hash_randomization,
+                         config_get('use_hash_seed') == 0
+                         or config_get('hash_seed') != 0)
+
+        if sysconfig.get_config_var('Py_GIL_DISABLED'):
+            value = config_get('enable_gil')
+            expected = (value if value != -1 else None)
+            self.assertEqual(sys.flags.gil, expected)
+
+    def test_config_get_non_existent(self):
+        # Test PyConfig_Get() on non-existent option name
+        config_get = _testcapi.config_get
+        nonexistent_key = 'NONEXISTENT_KEY'
+        err_msg = f'unknown config option name: {nonexistent_key}'
+        with self.assertRaisesRegex(ValueError, err_msg):
+            config_get(nonexistent_key)
+
+    def test_config_get_write_bytecode(self):
+        # PyConfig_Get("write_bytecode") gets sys.dont_write_bytecode
+        # as an integer
+        config_get = _testcapi.config_get
+        with support.swap_attr(sys, "dont_write_bytecode", 0):
+            self.assertEqual(config_get('write_bytecode'), 1)
+        with support.swap_attr(sys, "dont_write_bytecode", "yes"):
+            self.assertEqual(config_get('write_bytecode'), 0)
+        with support.swap_attr(sys, "dont_write_bytecode", []):
+            self.assertEqual(config_get('write_bytecode'), 1)
+
+    def test_config_getint(self):
+        # Test PyConfig_GetInt()
+        config_getint = _testcapi.config_getint
+
+        # PyConfig_MEMBER_INT type
+        self.assertEqual(config_getint('verbose'), sys.flags.verbose)
+
+        # PyConfig_MEMBER_UINT type
+        self.assertEqual(config_getint('isolated'), sys.flags.isolated)
+
+        # PyConfig_MEMBER_ULONG type
+        self.assertIsInstance(config_getint('hash_seed'), int)
+
+        # PyPreConfig member
+        self.assertIsInstance(config_getint('allocator'), int)
+
+        # platlibdir type is str
+        with self.assertRaises(TypeError):
+            config_getint('platlibdir')
+
+    def test_get_config_names(self):
+        names = _testcapi.config_names()
+        self.assertIsInstance(names, frozenset)
+        for name in names:
+            self.assertIsInstance(name, str)
+
+    def test_config_set_sys_attr(self):
+        # Test PyConfig_Set() with sys attributes
+        config_get = _testcapi.config_get
+        config_set = _testcapi.config_set
+
+        # mutable configuration option mapped to sys attributes
+        for name, sys_attr, option_type in (
+            ('argv', 'argv', list[str]),
+            ('base_exec_prefix', 'base_exec_prefix', str | None),
+            ('base_executable', '_base_executable', str | None),
+            ('base_prefix', 'base_prefix', str | None),
+            ('exec_prefix', 'exec_prefix', str | None),
+            ('executable', 'executable', str | None),
+            ('module_search_paths', 'path', list[str]),
+            ('platlibdir', 'platlibdir', str),
+            ('prefix', 'prefix', str | None),
+            ('pycache_prefix', 'pycache_prefix', str | None),
+            ('stdlib_dir', '_stdlib_dir', str | None),
+            ('warnoptions', 'warnoptions', list[str]),
+            ('xoptions', '_xoptions', dict[str, str | bool]),
+        ):
+            with self.subTest(name=name):
+                if option_type == str:
+                    test_values = ('TEST_REPLACE',)
+                    invalid_types = (1, None)
+                elif option_type == str | None:
+                    test_values = ('TEST_REPLACE', None)
+                    invalid_types = (123,)
+                elif option_type == list[str]:
+                    test_values = (['TEST_REPLACE'], [])
+                    invalid_types = ('text', 123, [123])
+                else:  # option_type == dict[str, str | bool]:
+                    test_values = ({"x": "value", "y": True},)
+                    invalid_types = ('text', 123, ['option'],
+                                     {123: 'value'},
+                                     {'key': b'bytes'})
+
+                old_opt_value = config_get(name)
+                old_sys_value = getattr(sys, sys_attr)
+                try:
+                    for value in test_values:
+                        config_set(name, value)
+                        self.assertEqual(config_get(name), value)
+                        self.assertEqual(getattr(sys, sys_attr), value)
+
+                    for value in invalid_types:
+                        with self.assertRaises(TypeError):
+                            config_set(name, value)
+                finally:
+                    setattr(sys, sys_attr, old_sys_value)
+                    config_set(name, old_opt_value)
+
+    def test_config_set_sys_flag(self):
+        # Test PyConfig_Set() with sys.flags
+        config_get = _testcapi.config_get
+        config_set = _testcapi.config_set
+
+        # mutable configuration option mapped to sys.flags
+        class unsigned_int(int):
+            pass
+
+        def expect_int(value):
+            value = int(value)
+            return (value, value)
+
+        def expect_bool(value):
+            value = int(bool(value))
+            return (value, value)
+
+        def expect_bool_not(value):
+            value = bool(value)
+            return (int(value), int(not value))
+
+        for name, sys_flag, option_type, expect_func in (
+            # (some flags cannot be set, see comments below.)
+            ('parser_debug', 'debug', bool, expect_bool),
+            ('inspect', 'inspect', bool, expect_bool),
+            ('interactive', 'interactive', bool, expect_bool),
+            ('optimization_level', 'optimize', unsigned_int, expect_int),
+            ('write_bytecode', 'dont_write_bytecode', bool, expect_bool_not),
+            # user_site_directory
+            # site_import
+            ('use_environment', 'ignore_environment', bool, expect_bool_not),
+            ('verbose', 'verbose', unsigned_int, expect_int),
+            ('bytes_warning', 'bytes_warning', unsigned_int, expect_int),
+            ('quiet', 'quiet', bool, expect_bool),
+            # hash_randomization
+            # isolated
+            # dev_mode
+            # utf8_mode
+            # warn_default_encoding
+            # safe_path
+            ('int_max_str_digits', 'int_max_str_digits', unsigned_int, expect_int),
+            # gil
+        ):
+            if name == "int_max_str_digits":
+                new_values = (0, 5_000, 999_999)
+                invalid_values = (-1, 40)  # value must 0 or >= 4300
+                invalid_types = (1.0, "abc")
+            elif option_type == int:
+                new_values = (False, True, 0, 1, 5, -5)
+                invalid_values = ()
+                invalid_types = (1.0, "abc")
+            else:
+                new_values = (False, True, 0, 1, 5)
+                invalid_values = (-5,)
+                invalid_types = (1.0, "abc")
+
+            with self.subTest(name=name):
+                old_value = config_get(name)
+                try:
+                    for value in new_values:
+                        expected, expect_flag = expect_func(value)
+
+                        config_set(name, value)
+                        self.assertEqual(config_get(name), expected)
+                        self.assertEqual(getattr(sys.flags, sys_flag), expect_flag)
+                        if name == "write_bytecode":
+                            self.assertEqual(getattr(sys, "dont_write_bytecode"),
+                                             expect_flag)
+                        if name == "int_max_str_digits":
+                            self.assertEqual(sys.get_int_max_str_digits(),
+                                             expect_flag)
+
+                    for value in invalid_values:
+                        with self.assertRaises(ValueError):
+                            config_set(name, value)
+
+                    for value in invalid_types:
+                        with self.assertRaises(TypeError):
+                            config_set(name, value)
+                finally:
+                    config_set(name, old_value)
+
+    def test_config_set_read_only(self):
+        # Test PyConfig_Set() on read-only options
+        config_set = _testcapi.config_set
+        for name, value in (
+            ("allocator", 0),  # PyPreConfig member
+            ("cpu_count", 8),
+            ("dev_mode", True),
+            ("filesystem_encoding", "utf-8"),
+        ):
+            with self.subTest(name=name, value=value):
+                with self.assertRaisesRegex(ValueError, r"read-only"):
+                    config_set(name, value)
+
+
+if __name__ == "__main__":
+    unittest.main()
index 7860c67f082b1fd1daced7820159db6dda1df63d..b1d1f6d06d708ea0dc7ca6508503b79b2853aa78 100644 (file)
@@ -508,30 +508,30 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
     PRE_CONFIG_COMPAT = {
         '_config_init': API_COMPAT,
         'allocator': PYMEM_ALLOCATOR_NOT_SET,
-        'parse_argv': 0,
-        'configure_locale': 1,
-        'coerce_c_locale': 0,
-        'coerce_c_locale_warn': 0,
-        'utf8_mode': 0,
+        'parse_argv': False,
+        'configure_locale': True,
+        'coerce_c_locale': False,
+        'coerce_c_locale_warn': False,
+        'utf8_mode': False,
     }
     if MS_WINDOWS:
         PRE_CONFIG_COMPAT.update({
-            'legacy_windows_fs_encoding': 0,
+            'legacy_windows_fs_encoding': False,
         })
     PRE_CONFIG_PYTHON = dict(PRE_CONFIG_COMPAT,
         _config_init=API_PYTHON,
-        parse_argv=1,
+        parse_argv=True,
         coerce_c_locale=GET_DEFAULT_CONFIG,
         utf8_mode=GET_DEFAULT_CONFIG,
     )
     PRE_CONFIG_ISOLATED = dict(PRE_CONFIG_COMPAT,
         _config_init=API_ISOLATED,
-        configure_locale=0,
-        isolated=1,
-        use_environment=0,
-        utf8_mode=0,
-        dev_mode=0,
-        coerce_c_locale=0,
+        configure_locale=False,
+        isolated=True,
+        use_environment=False,
+        utf8_mode=False,
+        dev_mode=False,
+        coerce_c_locale=False,
     )
 
     COPY_PRE_CONFIG = [
@@ -570,7 +570,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         'argv': [""],
         'orig_argv': [],
 
-        'xoptions': [],
+        'xoptions': {},
         'warnoptions': [],
 
         'pythonpath_env': None,
@@ -619,14 +619,14 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         '_is_python_build': IGNORE_CONFIG,
     }
     if Py_STATS:
-        CONFIG_COMPAT['_pystats'] = 0
+        CONFIG_COMPAT['_pystats'] = False
     if support.Py_DEBUG:
         CONFIG_COMPAT['run_presite'] = None
     if support.Py_GIL_DISABLED:
         CONFIG_COMPAT['enable_gil'] = -1
     if MS_WINDOWS:
         CONFIG_COMPAT.update({
-            'legacy_windows_stdio': 0,
+            'legacy_windows_stdio': False,
         })
 
     CONFIG_PYTHON = dict(CONFIG_COMPAT,
@@ -644,12 +644,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         install_signal_handlers=False,
         use_hash_seed=False,
         faulthandler=False,
-        tracemalloc=0,
+        tracemalloc=False,
         perf_profiling=False,
         pathconfig_warnings=False,
     )
     if MS_WINDOWS:
-        CONFIG_ISOLATED['legacy_windows_stdio'] = 0
+        CONFIG_ISOLATED['legacy_windows_stdio'] = False
 
     # global config
     DEFAULT_GLOBAL_CONFIG = {
@@ -928,23 +928,23 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
 
     def test_init_global_config(self):
         preconfig = {
-            'utf8_mode': 1,
+            'utf8_mode': True,
         }
         config = {
             'program_name': './globalvar',
-            'site_import': 0,
-            'bytes_warning': 1,
+            'site_import': False,
+            'bytes_warning': True,
             'warnoptions': ['default::BytesWarning'],
-            'inspect': 1,
-            'interactive': 1,
+            'inspect': True,
+            'interactive': True,
             'optimization_level': 2,
-            'write_bytecode': 0,
-            'verbose': 1,
-            'quiet': 1,
-            'buffered_stdio': 0,
+            'write_bytecode': False,
+            'verbose': True,
+            'quiet': True,
+            'buffered_stdio': False,
 
-            'user_site_directory': 0,
-            'pathconfig_warnings': 0,
+            'user_site_directory': False,
+            'pathconfig_warnings': False,
         }
         self.check_all_configs("test_init_global_config", config, preconfig,
                                api=API_COMPAT)
@@ -952,7 +952,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
     def test_init_from_config(self):
         preconfig = {
             'allocator': ALLOCATOR_FOR_CONFIG,
-            'utf8_mode': 1,
+            'utf8_mode': True,
         }
         config = {
             'install_signal_handlers': False,
@@ -977,12 +977,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
                           '-c', 'pass',
                           'arg2'],
             'parse_argv': True,
-            'xoptions': [
-                'config_xoption1=3',
-                'config_xoption2=',
-                'config_xoption3',
-                'cmdline_xoption',
-            ],
+            'xoptions': {
+                'config_xoption1': '3',
+                'config_xoption2': '',
+                'config_xoption3': True,
+                'cmdline_xoption': True,
+            },
             'warnoptions': [
                 'cmdline_warnoption',
                 'default::BytesWarning',
@@ -1126,7 +1126,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
             'dev_mode': True,
             'faulthandler': True,
             'warnoptions': ['default'],
-            'xoptions': ['dev'],
+            'xoptions': {'dev': True},
             'safe_path': True,
         }
         self.check_all_configs("test_preinit_parse_argv", config, preconfig,
@@ -1135,7 +1135,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
     def test_preinit_dont_parse_argv(self):
         # -X dev must be ignored by isolated preconfiguration
         preconfig = {
-            'isolated': 0,
+            'isolated': False,
         }
         argv = ["python3",
                "-E", "-I", "-P",
@@ -1145,7 +1145,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         config = {
             'argv': argv,
             'orig_argv': argv,
-            'isolated': 0,
+            'isolated': False,
         }
         self.check_all_configs("test_preinit_dont_parse_argv", config, preconfig,
                                api=API_ISOLATED)
@@ -1218,12 +1218,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
     def test_init_sys_add(self):
         config = {
             'faulthandler': 1,
-            'xoptions': [
-                'config_xoption',
-                'cmdline_xoption',
-                'sysadd_xoption',
-                'faulthandler',
-            ],
+            'xoptions': {
+                'config_xoption': True,
+                'cmdline_xoption': True,
+                'sysadd_xoption': True,
+                'faulthandler': True,
+            },
             'warnoptions': [
                 'ignore:::cmdline_warnoption',
                 'ignore:::sysadd_warnoption',
@@ -1259,7 +1259,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
             'program_name': './python3',
             'run_command': code + '\n',
             'parse_argv': True,
-            '_init_main': 0,
+            '_init_main': False,
             'sys_path_0': '',
         }
         self.check_all_configs("test_init_main", config,
@@ -1637,12 +1637,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
                 config['base_prefix'] = pyvenv_home
                 config['prefix'] = pyvenv_home
                 config['stdlib_dir'] = os.path.join(pyvenv_home, 'Lib')
-                config['use_frozen_modules'] = int(not support.Py_DEBUG)
+                config['use_frozen_modules'] = bool(not support.Py_DEBUG)
             else:
                 # cannot reliably assume stdlib_dir here because it
                 # depends too much on our build. But it ought to be found
                 config['stdlib_dir'] = self.IGNORE_CONFIG
-                config['use_frozen_modules'] = int(not support.Py_DEBUG)
+                config['use_frozen_modules'] = bool(not support.Py_DEBUG)
 
             env = self.copy_paths_by_env(config)
             self.check_all_configs("test_init_compat_config", config,
@@ -1706,7 +1706,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
 
         config = _testinternalcapi.get_configs()['config']
 
-        self.assertEqual(Py_GetPath().split(os.path.pathsep),
+        self.assertEqual(tuple(Py_GetPath().split(os.path.pathsep)),
                          config['module_search_paths'])
         self.assertEqual(Py_GetPrefix(), config['prefix'])
         self.assertEqual(Py_GetExecPrefix(), config['exec_prefix'])
@@ -1763,6 +1763,10 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         }
         for raw, expected in tests:
             optval = f'frozen_modules{raw}'
+            if raw.startswith('='):
+                xoption_value = raw[1:]
+            else:
+                xoption_value = True
             config = {
                 'parse_argv': True,
                 'argv': ['-c'],
@@ -1770,7 +1774,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
                 'program_name': './argv0',
                 'run_command': 'pass\n',
                 'use_environment': True,
-                'xoptions': [optval],
+                'xoptions': {'frozen_modules': xoption_value},
                 'use_frozen_modules': expected,
             }
             env = {'TESTFROZEN': raw[1:]} if raw else None
diff --git a/Misc/NEWS.d/next/C_API/2024-08-29-15-55-55.gh-issue-107954.pr2O50.rst b/Misc/NEWS.d/next/C_API/2024-08-29-15-55-55.gh-issue-107954.pr2O50.rst
new file mode 100644 (file)
index 0000000..f111687
--- /dev/null
@@ -0,0 +1,8 @@
+Add functions to get and set the current runtime Python configuration:
+
+* :c:func:`PyConfig_Get`
+* :c:func:`PyConfig_GetInt`
+* :c:func:`PyConfig_Set`
+* :c:func:`PyConfig_Names`
+
+Patch by Victor Stinner.
index 9121a8c5dc69e00d5c85e9bd77f60567ae538989..9aa398a80efa1bdc152f6626e38f2b0607156ea3 100644 (file)
 @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
 @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
 @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
-@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c
+@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c  _testcapi/config.c
 @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c
 @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
 @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
diff --git a/Modules/_testcapi/config.c b/Modules/_testcapi/config.c
new file mode 100644 (file)
index 0000000..bb3b7e8
--- /dev/null
@@ -0,0 +1,68 @@
+#include "parts.h"
+
+
+static PyObject *
+_testcapi_config_get(PyObject *module, PyObject *name_obj)
+{
+    const char *name;
+    if (PyArg_Parse(name_obj, "s", &name) < 0) {
+        return NULL;
+    }
+
+    return PyConfig_Get(name);
+}
+
+
+static PyObject *
+_testcapi_config_getint(PyObject *module, PyObject *name_obj)
+{
+    const char *name;
+    if (PyArg_Parse(name_obj, "s", &name) < 0) {
+        return NULL;
+    }
+
+    int value;
+    if (PyConfig_GetInt(name, &value) < 0) {
+        return NULL;
+    }
+    return PyLong_FromLong(value);
+}
+
+
+static PyObject *
+_testcapi_config_names(PyObject *module, PyObject* Py_UNUSED(args))
+{
+    return PyConfig_Names();
+}
+
+
+static PyObject *
+_testcapi_config_set(PyObject *module, PyObject *args)
+{
+    const char *name;
+    PyObject *value;
+    if (PyArg_ParseTuple(args, "sO", &name, &value) < 0) {
+        return NULL;
+    }
+
+    int res = PyConfig_Set(name, value);
+    if (res < 0) {
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+
+static PyMethodDef test_methods[] = {
+    {"config_get", _testcapi_config_get, METH_O},
+    {"config_getint", _testcapi_config_getint, METH_O},
+    {"config_names", _testcapi_config_names, METH_NOARGS},
+    {"config_set", _testcapi_config_set, METH_VARARGS},
+    {NULL}
+};
+
+int
+_PyTestCapi_Init_Config(PyObject *mod)
+{
+    return PyModule_AddFunctions(mod, test_methods);
+}
index 41d190961c69ee47a3266d643624ece5702d6ba0..65ba77596c760eaee478ac687925147f7049036b 100644 (file)
@@ -60,5 +60,6 @@ int _PyTestCapi_Init_Hash(PyObject *module);
 int _PyTestCapi_Init_Time(PyObject *module);
 int _PyTestCapi_Init_Monitoring(PyObject *module);
 int _PyTestCapi_Init_Object(PyObject *module);
+int _PyTestCapi_Init_Config(PyObject *mod);
 
 #endif // Py_TESTCAPI_PARTS_H
index 7ffa87dcd10274a5bb5e39cfaffd671d849baff6..5966eb674cf4e58fb742479d16be9f566696efcd 100644 (file)
@@ -4172,6 +4172,9 @@ PyInit__testcapi(void)
     if (_PyTestCapi_Init_Object(m) < 0) {
         return NULL;
     }
+    if (_PyTestCapi_Init_Config(m) < 0) {
+        return NULL;
+    }
 
     PyState_AddModule(m, &_testcapimodule);
     return m;
index 44dbf2348137e17ee65dd76002c2eae073270cc4..c41235eac356af2aff1c1f70634b77e942957eb8 100644 (file)
     <ClCompile Include="..\Modules\_testcapi\gc.c" />
     <ClCompile Include="..\Modules\_testcapi\run.c" />
     <ClCompile Include="..\Modules\_testcapi\monitoring.c" />
+    <ClCompile Include="..\Modules\_testcapi\config.c" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\PC\python_nt.rc" />
index cae44bc955f7f12748b98f990a5eab72f1efbcec..0a00df655deefcdd3073798facf5af880d301bb1 100644 (file)
     <ClCompile Include="..\Modules\_testcapi\monitoring.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Modules\_testcapi\config.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\PC\python_nt.rc">
index 51897a2d0aef66bd2f84c68c1470ee7a5d1d88e3..1c21486d8f2372855122c8c644b1ec8e748ea4a6 100644 (file)
@@ -10,6 +10,7 @@
 #include "pycore_pymem.h"         // _PyMem_SetDefaultAllocator()
 #include "pycore_pystate.h"       // _PyThreadState_GET()
 #include "pycore_pystats.h"       // _Py_StatsOn()
+#include "pycore_sysmodule.h"     // _PySys_SetIntMaxStrDigits()
 
 #include "osdefs.h"               // DELIM
 
 
 #include "config_common.h"
 
+/* --- PyConfig setters ------------------------------------------- */
+
+typedef PyObject* (*config_sys_flag_setter) (int value);
+
+static PyObject*
+config_sys_flag_long(int value)
+{
+    return PyLong_FromLong(value);
+}
+
+static PyObject*
+config_sys_flag_not(int value)
+{
+    value = (!value);
+    return config_sys_flag_long(value);
+}
 
 /* --- PyConfig spec ---------------------------------------------- */
 
@@ -40,99 +57,174 @@ typedef enum {
     PyConfig_MEMBER_WSTR_LIST = 12,
 } PyConfigMemberType;
 
+typedef enum {
+    // Option which cannot be get or set by PyConfig_Get() and PyConfig_Set()
+    PyConfig_MEMBER_INIT_ONLY = 0,
+
+    // Option which cannot be set by PyConfig_Set()
+    PyConfig_MEMBER_READ_ONLY = 1,
+
+    // Public option: can be get and set by PyConfig_Get() and PyConfig_Set()
+    PyConfig_MEMBER_PUBLIC = 2,
+} PyConfigMemberVisibility;
+
+typedef struct {
+    const char *attr;
+    int flag_index;
+    config_sys_flag_setter flag_setter;
+} PyConfigSysSpec;
+
 typedef struct {
     const char *name;
     size_t offset;
     PyConfigMemberType type;
+    PyConfigMemberVisibility visibility;
+    PyConfigSysSpec sys;
 } PyConfigSpec;
 
-#define SPEC(MEMBER, TYPE) \
-    {#MEMBER, offsetof(PyConfig, MEMBER), PyConfig_MEMBER_##TYPE}
+#define SPEC(MEMBER, TYPE, VISIBILITY, sys) \
+    {#MEMBER, offsetof(PyConfig, MEMBER), \
+     PyConfig_MEMBER_##TYPE, PyConfig_MEMBER_##VISIBILITY, sys}
+
+#define SYS_ATTR(name) {name, -1, NULL}
+#define SYS_FLAG_SETTER(index, setter) {NULL, index, setter}
+#define SYS_FLAG(index) SYS_FLAG_SETTER(index, NULL)
+#define NO_SYS SYS_ATTR(NULL)
 
 // Update _test_embed_set_config when adding new members
 static const PyConfigSpec PYCONFIG_SPEC[] = {
-    SPEC(_config_init, UINT),
-    SPEC(isolated, BOOL),
-    SPEC(use_environment, BOOL),
-    SPEC(dev_mode, BOOL),
-    SPEC(install_signal_handlers, BOOL),
-    SPEC(use_hash_seed, BOOL),
-    SPEC(hash_seed, ULONG),
-    SPEC(faulthandler, BOOL),
-    SPEC(tracemalloc, UINT),
-    SPEC(perf_profiling, UINT),
-    SPEC(import_time, BOOL),
-    SPEC(code_debug_ranges, BOOL),
-    SPEC(show_ref_count, BOOL),
-    SPEC(dump_refs, BOOL),
-    SPEC(dump_refs_file, WSTR_OPT),
-    SPEC(malloc_stats, BOOL),
-    SPEC(filesystem_encoding, WSTR),
-    SPEC(filesystem_errors, WSTR),
-    SPEC(pycache_prefix, WSTR_OPT),
-    SPEC(parse_argv, BOOL),
-    SPEC(orig_argv, WSTR_LIST),
-    SPEC(argv, WSTR_LIST),
-    SPEC(xoptions, WSTR_LIST),
-    SPEC(warnoptions, WSTR_LIST),
-    SPEC(site_import, BOOL),
-    SPEC(bytes_warning, UINT),
-    SPEC(warn_default_encoding, BOOL),
-    SPEC(inspect, BOOL),
-    SPEC(interactive, BOOL),
-    SPEC(optimization_level, UINT),
-    SPEC(parser_debug, BOOL),
-    SPEC(write_bytecode, BOOL),
-    SPEC(verbose, UINT),
-    SPEC(quiet, BOOL),
-    SPEC(user_site_directory, BOOL),
-    SPEC(configure_c_stdio, BOOL),
-    SPEC(buffered_stdio, BOOL),
-    SPEC(stdio_encoding, WSTR),
-    SPEC(stdio_errors, WSTR),
-#ifdef MS_WINDOWS
-    SPEC(legacy_windows_stdio, BOOL),
+    // --- Public options -----------
+
+    SPEC(argv, WSTR_LIST, PUBLIC, SYS_ATTR("argv")),
+    SPEC(base_exec_prefix, WSTR_OPT, PUBLIC, SYS_ATTR("base_exec_prefix")),
+    SPEC(base_executable, WSTR_OPT, PUBLIC, SYS_ATTR("_base_executable")),
+    SPEC(base_prefix, WSTR_OPT, PUBLIC, SYS_ATTR("base_prefix")),
+    SPEC(bytes_warning, UINT, PUBLIC, SYS_FLAG(9)),
+    SPEC(exec_prefix, WSTR_OPT, PUBLIC, SYS_ATTR("exec_prefix")),
+    SPEC(executable, WSTR_OPT, PUBLIC, SYS_ATTR("executable")),
+    SPEC(inspect, BOOL, PUBLIC, SYS_FLAG(1)),
+    SPEC(int_max_str_digits, UINT, PUBLIC, NO_SYS),
+    SPEC(interactive, BOOL, PUBLIC, SYS_FLAG(2)),
+    SPEC(module_search_paths, WSTR_LIST, PUBLIC, SYS_ATTR("path")),
+    SPEC(optimization_level, UINT, PUBLIC, SYS_FLAG(3)),
+    SPEC(parser_debug, BOOL, PUBLIC, SYS_FLAG(0)),
+    SPEC(platlibdir, WSTR, PUBLIC, SYS_ATTR("platlibdir")),
+    SPEC(prefix, WSTR_OPT, PUBLIC, SYS_ATTR("prefix")),
+    SPEC(pycache_prefix, WSTR_OPT, PUBLIC, SYS_ATTR("pycache_prefix")),
+    SPEC(quiet, BOOL, PUBLIC, SYS_FLAG(10)),
+    SPEC(stdlib_dir, WSTR_OPT, PUBLIC, SYS_ATTR("_stdlib_dir")),
+    SPEC(use_environment, BOOL, PUBLIC, SYS_FLAG_SETTER(7, config_sys_flag_not)),
+    SPEC(verbose, UINT, PUBLIC, SYS_FLAG(8)),
+    SPEC(warnoptions, WSTR_LIST, PUBLIC, SYS_ATTR("warnoptions")),
+    SPEC(write_bytecode, BOOL, PUBLIC, SYS_FLAG_SETTER(4, config_sys_flag_not)),
+    SPEC(xoptions, WSTR_LIST, PUBLIC, SYS_ATTR("_xoptions")),
+
+    // --- Read-only options -----------
+
+#ifdef Py_STATS
+    SPEC(_pystats, BOOL, READ_ONLY, NO_SYS),
 #endif
-    SPEC(check_hash_pycs_mode, WSTR),
-    SPEC(use_frozen_modules, BOOL),
-    SPEC(safe_path, BOOL),
-    SPEC(int_max_str_digits, INT),
-    SPEC(cpu_count, INT),
+    SPEC(buffered_stdio, BOOL, READ_ONLY, NO_SYS),
+    SPEC(check_hash_pycs_mode, WSTR, READ_ONLY, NO_SYS),
+    SPEC(code_debug_ranges, BOOL, READ_ONLY, NO_SYS),
+    SPEC(configure_c_stdio, BOOL, READ_ONLY, NO_SYS),
+    SPEC(cpu_count, INT, READ_ONLY, NO_SYS),
+    SPEC(dev_mode, BOOL, READ_ONLY, NO_SYS),  // sys.flags.dev_mode
+    SPEC(dump_refs, BOOL, READ_ONLY, NO_SYS),
+    SPEC(dump_refs_file, WSTR_OPT, READ_ONLY, NO_SYS),
 #ifdef Py_GIL_DISABLED
-    SPEC(enable_gil, INT),
+    SPEC(enable_gil, INT, READ_ONLY, NO_SYS),
 #endif
-    SPEC(pathconfig_warnings, BOOL),
-    SPEC(program_name, WSTR),
-    SPEC(pythonpath_env, WSTR_OPT),
-    SPEC(home, WSTR_OPT),
-    SPEC(platlibdir, WSTR),
-    SPEC(sys_path_0, WSTR_OPT),
-    SPEC(module_search_paths_set, BOOL),
-    SPEC(module_search_paths, WSTR_LIST),
-    SPEC(stdlib_dir, WSTR_OPT),
-    SPEC(executable, WSTR_OPT),
-    SPEC(base_executable, WSTR_OPT),
-    SPEC(prefix, WSTR_OPT),
-    SPEC(base_prefix, WSTR_OPT),
-    SPEC(exec_prefix, WSTR_OPT),
-    SPEC(base_exec_prefix, WSTR_OPT),
-    SPEC(skip_source_first_line, BOOL),
-    SPEC(run_command, WSTR_OPT),
-    SPEC(run_module, WSTR_OPT),
-    SPEC(run_filename, WSTR_OPT),
-    SPEC(_install_importlib, BOOL),
-    SPEC(_init_main, BOOL),
-    SPEC(_is_python_build, BOOL),
-#ifdef Py_STATS
-    SPEC(_pystats, BOOL),
+    SPEC(faulthandler, BOOL, READ_ONLY, NO_SYS),
+    SPEC(filesystem_encoding, WSTR, READ_ONLY, NO_SYS),
+    SPEC(filesystem_errors, WSTR, READ_ONLY, NO_SYS),
+    SPEC(hash_seed, ULONG, READ_ONLY, NO_SYS),
+    SPEC(home, WSTR_OPT, READ_ONLY, NO_SYS),
+    SPEC(import_time, BOOL, READ_ONLY, NO_SYS),
+    SPEC(install_signal_handlers, BOOL, READ_ONLY, NO_SYS),
+    SPEC(isolated, BOOL, READ_ONLY, NO_SYS),  // sys.flags.isolated
+#ifdef MS_WINDOWS
+    SPEC(legacy_windows_stdio, BOOL, READ_ONLY, NO_SYS),
 #endif
+    SPEC(malloc_stats, BOOL, READ_ONLY, NO_SYS),
+    SPEC(orig_argv, WSTR_LIST, READ_ONLY, SYS_ATTR("orig_argv")),
+    SPEC(parse_argv, BOOL, READ_ONLY, NO_SYS),
+    SPEC(pathconfig_warnings, BOOL, READ_ONLY, NO_SYS),
+    SPEC(perf_profiling, BOOL, READ_ONLY, NO_SYS),
+    SPEC(program_name, WSTR, READ_ONLY, NO_SYS),
+    SPEC(run_command, WSTR_OPT, READ_ONLY, NO_SYS),
+    SPEC(run_filename, WSTR_OPT, READ_ONLY, NO_SYS),
+    SPEC(run_module, WSTR_OPT, READ_ONLY, NO_SYS),
 #ifdef Py_DEBUG
-    SPEC(run_presite, WSTR_OPT),
+    SPEC(run_presite, WSTR_OPT, READ_ONLY, NO_SYS),
+#endif
+    SPEC(safe_path, BOOL, READ_ONLY, NO_SYS),
+    SPEC(show_ref_count, BOOL, READ_ONLY, NO_SYS),
+    SPEC(site_import, BOOL, READ_ONLY, NO_SYS),  // sys.flags.no_site
+    SPEC(skip_source_first_line, BOOL, READ_ONLY, NO_SYS),
+    SPEC(stdio_encoding, WSTR, READ_ONLY, NO_SYS),
+    SPEC(stdio_errors, WSTR, READ_ONLY, NO_SYS),
+    SPEC(tracemalloc, UINT, READ_ONLY, NO_SYS),
+    SPEC(use_frozen_modules, BOOL, READ_ONLY, NO_SYS),
+    SPEC(use_hash_seed, BOOL, READ_ONLY, NO_SYS),
+    SPEC(user_site_directory, BOOL, READ_ONLY, NO_SYS),  // sys.flags.no_user_site
+    SPEC(warn_default_encoding, BOOL, READ_ONLY, NO_SYS),
+
+    // --- Init-only options -----------
+
+    SPEC(_config_init, UINT, INIT_ONLY, NO_SYS),
+    SPEC(_init_main, BOOL, INIT_ONLY, NO_SYS),
+    SPEC(_install_importlib, BOOL, INIT_ONLY, NO_SYS),
+    SPEC(_is_python_build, BOOL, INIT_ONLY, NO_SYS),
+    SPEC(module_search_paths_set, BOOL, INIT_ONLY, NO_SYS),
+    SPEC(pythonpath_env, WSTR_OPT, INIT_ONLY, NO_SYS),
+    SPEC(sys_path_0, WSTR_OPT, INIT_ONLY, NO_SYS),
+
+    // Array terminator
+    {NULL, 0, 0, 0, NO_SYS},
+};
+
+#undef SPEC
+#define SPEC(MEMBER, TYPE, VISIBILITY) \
+    {#MEMBER, offsetof(PyPreConfig, MEMBER), PyConfig_MEMBER_##TYPE, \
+     PyConfig_MEMBER_##VISIBILITY, NO_SYS}
+
+static const PyConfigSpec PYPRECONFIG_SPEC[] = {
+    // --- Read-only options -----------
+
+    SPEC(allocator, INT, READ_ONLY),
+    SPEC(coerce_c_locale, BOOL, READ_ONLY),
+    SPEC(coerce_c_locale_warn, BOOL, READ_ONLY),
+    SPEC(configure_locale, BOOL, READ_ONLY),
+#ifdef MS_WINDOWS
+    SPEC(legacy_windows_fs_encoding, BOOL, READ_ONLY),
 #endif
-    {NULL, 0, 0},
+    SPEC(utf8_mode, BOOL, READ_ONLY),
+
+    // --- Init-only options -----------
+    // Members already present in PYCONFIG_SPEC
+
+    SPEC(_config_init, INT, INIT_ONLY),
+    SPEC(dev_mode, BOOL, INIT_ONLY),
+    SPEC(isolated, BOOL, INIT_ONLY),
+    SPEC(parse_argv, BOOL, INIT_ONLY),
+    SPEC(use_environment, BOOL, INIT_ONLY),
+
+    // Array terminator
+    {NULL, 0, 0, 0, NO_SYS},
 };
 
 #undef SPEC
+#undef SYS_ATTR
+#undef SYS_FLAG_SETTER
+#undef SYS_FLAG
+#undef NO_SYS
+
+
+// Forward declarations
+static PyObject*
+config_get(const PyConfig *config, const PyConfigSpec *spec,
+           int use_sys);
 
 
 /* --- Command line options --------------------------------------- */
@@ -656,6 +748,28 @@ _PyWideStringList_AsList(const PyWideStringList *list)
 }
 
 
+static PyObject*
+_PyWideStringList_AsTuple(const PyWideStringList *list)
+{
+    assert(_PyWideStringList_CheckConsistency(list));
+
+    PyObject *tuple = PyTuple_New(list->length);
+    if (tuple == NULL) {
+        return NULL;
+    }
+
+    for (Py_ssize_t i = 0; i < list->length; i++) {
+        PyObject *item = PyUnicode_FromWideChar(list->items[i], -1);
+        if (item == NULL) {
+            Py_DECREF(tuple);
+            return NULL;
+        }
+        PyTuple_SET_ITEM(tuple, i, item);
+    }
+    return tuple;
+}
+
+
 /* --- Py_GetArgcArgv() ------------------------------------------- */
 
 void
@@ -1059,54 +1173,12 @@ _PyConfig_AsDict(const PyConfig *config)
 
     const PyConfigSpec *spec = PYCONFIG_SPEC;
     for (; spec->name != NULL; spec++) {
-        char *member = (char *)config + spec->offset;
-        PyObject *obj;
-        switch (spec->type) {
-        case PyConfig_MEMBER_INT:
-        case PyConfig_MEMBER_UINT:
-        {
-            int value = *(int*)member;
-            obj = PyLong_FromLong(value);
-            break;
-        }
-        case PyConfig_MEMBER_BOOL:
-        {
-            int value = *(int*)member;
-            obj = PyBool_FromLong(value);
-            break;
-        }
-        case PyConfig_MEMBER_ULONG:
-        {
-            unsigned long value = *(unsigned long*)member;
-            obj = PyLong_FromUnsignedLong(value);
-            break;
-        }
-        case PyConfig_MEMBER_WSTR:
-        case PyConfig_MEMBER_WSTR_OPT:
-        {
-            const wchar_t *wstr = *(const wchar_t**)member;
-            if (wstr != NULL) {
-                obj = PyUnicode_FromWideChar(wstr, -1);
-            }
-            else {
-                obj = Py_NewRef(Py_None);
-            }
-            break;
-        }
-        case PyConfig_MEMBER_WSTR_LIST:
-        {
-            const PyWideStringList *list = (const PyWideStringList*)member;
-            obj = _PyWideStringList_AsList(list);
-            break;
-        }
-        default:
-            Py_UNREACHABLE();
-        }
-
+        PyObject *obj = config_get(config, spec, 0);
         if (obj == NULL) {
             Py_DECREF(dict);
             return NULL;
         }
+
         int res = PyDict_SetItemString(dict, spec->name, obj);
         Py_DECREF(obj);
         if (res < 0) {
@@ -1218,15 +1290,17 @@ config_dict_get_wstrlist(PyObject *dict, const char *name, PyConfig *config,
         return -1;
     }
 
-    if (!PyList_CheckExact(list)) {
+    int is_list = PyList_CheckExact(list);
+    if (!is_list && !PyTuple_CheckExact(list)) {
         Py_DECREF(list);
         config_dict_invalid_type(name);
         return -1;
     }
 
     PyWideStringList wstrlist = _PyWideStringList_INIT;
-    for (Py_ssize_t i=0; i < PyList_GET_SIZE(list); i++) {
-        PyObject *item = PyList_GET_ITEM(list, i);
+    Py_ssize_t len = is_list ? PyList_GET_SIZE(list) : PyTuple_GET_SIZE(list);
+    for (Py_ssize_t i=0; i < len; i++) {
+        PyObject *item = is_list ? PyList_GET_ITEM(list, i) : PyTuple_GET_ITEM(list, i);
 
         if (item == Py_None) {
             config_dict_invalid_value(name);
@@ -1263,6 +1337,66 @@ error:
 }
 
 
+static int
+config_dict_get_xoptions(PyObject *dict, const char *name, PyConfig *config,
+                         PyWideStringList *result)
+{
+    PyObject *xoptions = config_dict_get(dict, name);
+    if (xoptions == NULL) {
+        return -1;
+    }
+
+    if (!PyDict_CheckExact(xoptions)) {
+        Py_DECREF(xoptions);
+        config_dict_invalid_type(name);
+        return -1;
+    }
+
+    Py_ssize_t pos = 0;
+    PyObject *key, *value;
+    PyWideStringList wstrlist = _PyWideStringList_INIT;
+    while (PyDict_Next(xoptions, &pos, &key, &value)) {
+        PyObject *item;
+
+        if (value != Py_True) {
+            item = PyUnicode_FromFormat("%S=%S", key, value);
+            if (item == NULL) {
+                goto error;
+            }
+        }
+        else {
+            item = Py_NewRef(key);
+        }
+
+        wchar_t *wstr = PyUnicode_AsWideCharString(item, NULL);
+        Py_DECREF(item);
+        if (wstr == NULL) {
+            goto error;
+        }
+
+        PyStatus status = PyWideStringList_Append(&wstrlist, wstr);
+        PyMem_Free(wstr);
+        if (_PyStatus_EXCEPTION(status)) {
+            PyErr_NoMemory();
+            goto error;
+        }
+    }
+
+    if (_PyWideStringList_Copy(result, &wstrlist) < 0) {
+        PyErr_NoMemory();
+        goto error;
+    }
+    _PyWideStringList_Clear(&wstrlist);
+    Py_DECREF(xoptions);
+    return 0;
+
+error:
+    _PyWideStringList_Clear(&wstrlist);
+    Py_DECREF(xoptions);
+    return -1;
+}
+
+
 int
 _PyConfig_FromDict(PyConfig *config, PyObject *dict)
 {
@@ -1324,9 +1458,17 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
         }
         case PyConfig_MEMBER_WSTR_LIST:
         {
-            if (config_dict_get_wstrlist(dict, spec->name, config,
-                                         (PyWideStringList*)member) < 0) {
-                return -1;
+            if (strcmp(spec->name, "xoptions") == 0) {
+                if (config_dict_get_xoptions(dict, spec->name, config,
+                                             (PyWideStringList*)member) < 0) {
+                    return -1;
+                }
+            }
+            else {
+                if (config_dict_get_wstrlist(dict, spec->name, config,
+                                             (PyWideStringList*)member) < 0) {
+                    return -1;
+                }
             }
             break;
         }
@@ -3261,3 +3403,505 @@ _Py_DumpPathConfig(PyThreadState *tstate)
 
     _PyErr_SetRaisedException(tstate, exc);
 }
+
+
+// --- PyConfig_Get() -------------------------------------------------------
+
+static void*
+config_spec_get_member(const PyConfigSpec *spec, const PyConfig *config)
+{
+    return (char *)config + spec->offset;
+}
+
+
+static const PyConfigSpec*
+config_generic_find_spec(const PyConfigSpec *spec, const char *name)
+{
+    for (; spec->name != NULL; spec++) {
+        if (spec->visibility == PyConfig_MEMBER_INIT_ONLY) {
+            continue;
+        }
+        if (strcmp(name, spec->name) == 0) {
+            return spec;
+        }
+    }
+    return NULL;
+}
+
+
+static const PyConfigSpec*
+config_find_spec(const char *name)
+{
+    return config_generic_find_spec(PYCONFIG_SPEC, name);
+}
+
+
+static const PyConfigSpec*
+preconfig_find_spec(const char *name)
+{
+    return config_generic_find_spec(PYPRECONFIG_SPEC, name);
+}
+
+
+static int
+config_add_xoption(PyObject *dict, const wchar_t *str)
+{
+    PyObject *name = NULL, *value = NULL;
+
+    const wchar_t *name_end = wcschr(str, L'=');
+    if (!name_end) {
+        name = PyUnicode_FromWideChar(str, -1);
+        if (name == NULL) {
+            goto error;
+        }
+        value = Py_NewRef(Py_True);
+    }
+    else {
+        name = PyUnicode_FromWideChar(str, name_end - str);
+        if (name == NULL) {
+            goto error;
+        }
+        value = PyUnicode_FromWideChar(name_end + 1, -1);
+        if (value == NULL) {
+            goto error;
+        }
+    }
+    if (PyDict_SetItem(dict, name, value) < 0) {
+        goto error;
+    }
+    Py_DECREF(name);
+    Py_DECREF(value);
+    return 0;
+
+error:
+    Py_XDECREF(name);
+    Py_XDECREF(value);
+    return -1;
+}
+
+
+PyObject*
+_PyConfig_CreateXOptionsDict(const PyConfig *config)
+{
+    PyObject *dict = PyDict_New();
+    if (dict == NULL) {
+        return NULL;
+    }
+
+    Py_ssize_t nxoption = config->xoptions.length;
+    wchar_t **xoptions = config->xoptions.items;
+    for (Py_ssize_t i=0; i < nxoption; i++) {
+        const wchar_t *option = xoptions[i];
+        if (config_add_xoption(dict, option) < 0) {
+            Py_DECREF(dict);
+            return NULL;
+        }
+    }
+    return dict;
+}
+
+
+static PyObject*
+config_get_sys(const char *name)
+{
+    PyObject *value = PySys_GetObject(name);
+    if (value == NULL) {
+        PyErr_Format(PyExc_RuntimeError, "lost sys.%s", name);
+        return NULL;
+    }
+    return Py_NewRef(value);
+}
+
+
+static int
+config_get_sys_write_bytecode(const PyConfig *config, int *value)
+{
+    PyObject *attr = config_get_sys("dont_write_bytecode");
+    if (attr == NULL) {
+        return -1;
+    }
+
+    int is_true = PyObject_IsTrue(attr);
+    Py_DECREF(attr);
+    if (is_true < 0) {
+        return -1;
+    }
+    *value = (!is_true);
+    return 0;
+}
+
+
+static PyObject*
+config_get(const PyConfig *config, const PyConfigSpec *spec,
+           int use_sys)
+{
+    if (use_sys) {
+        if (spec->sys.attr != NULL) {
+            return config_get_sys(spec->sys.attr);
+        }
+
+        if (strcmp(spec->name, "write_bytecode") == 0) {
+            int value;
+            if (config_get_sys_write_bytecode(config, &value) < 0) {
+                return NULL;
+            }
+            return PyBool_FromLong(value);
+        }
+
+        if (strcmp(spec->name, "int_max_str_digits") == 0) {
+            PyInterpreterState *interp = _PyInterpreterState_GET();
+            return PyLong_FromLong(interp->long_state.max_str_digits);
+        }
+    }
+
+    void *member = config_spec_get_member(spec, config);
+    switch (spec->type) {
+    case PyConfig_MEMBER_INT:
+    case PyConfig_MEMBER_UINT:
+    {
+        int value = *(int *)member;
+        return PyLong_FromLong(value);
+    }
+
+    case PyConfig_MEMBER_BOOL:
+    {
+        int value = *(int *)member;
+        return PyBool_FromLong(value != 0);
+    }
+
+    case PyConfig_MEMBER_ULONG:
+    {
+        unsigned long value = *(unsigned long *)member;
+        return PyLong_FromUnsignedLong(value);
+    }
+
+    case PyConfig_MEMBER_WSTR:
+    case PyConfig_MEMBER_WSTR_OPT:
+    {
+        wchar_t *wstr = *(wchar_t **)member;
+        if (wstr != NULL) {
+            return PyUnicode_FromWideChar(wstr, -1);
+        }
+        else {
+            return Py_NewRef(Py_None);
+        }
+    }
+
+    case PyConfig_MEMBER_WSTR_LIST:
+    {
+        if (strcmp(spec->name, "xoptions") == 0) {
+            return _PyConfig_CreateXOptionsDict(config);
+        }
+        else {
+            const PyWideStringList *list = (const PyWideStringList *)member;
+            return _PyWideStringList_AsTuple(list);
+        }
+    }
+
+    default:
+        Py_UNREACHABLE();
+    }
+}
+
+
+static PyObject*
+preconfig_get(const PyPreConfig *preconfig, const PyConfigSpec *spec)
+{
+    // The type of all PYPRECONFIG_SPEC members is INT or BOOL.
+    assert(spec->type == PyConfig_MEMBER_INT
+           || spec->type == PyConfig_MEMBER_BOOL);
+
+    char *member = (char *)preconfig + spec->offset;
+    int value = *(int *)member;
+
+    if (spec->type == PyConfig_MEMBER_BOOL) {
+        return PyBool_FromLong(value != 0);
+    }
+    else {
+        return PyLong_FromLong(value);
+    }
+}
+
+
+static void
+config_unknown_name_error(const char *name)
+{
+    PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name);
+}
+
+
+PyObject*
+PyConfig_Get(const char *name)
+{
+    const PyConfigSpec *spec = config_find_spec(name);
+    if (spec != NULL) {
+        const PyConfig *config = _Py_GetConfig();
+        return config_get(config, spec, 1);
+    }
+
+    spec = preconfig_find_spec(name);
+    if (spec != NULL) {
+        const PyPreConfig *preconfig = &_PyRuntime.preconfig;
+        return preconfig_get(preconfig, spec);
+    }
+
+    config_unknown_name_error(name);
+    return NULL;
+}
+
+
+int
+PyConfig_GetInt(const char *name, int *value)
+{
+    assert(!PyErr_Occurred());
+
+    PyObject *obj = PyConfig_Get(name);
+    if (obj == NULL) {
+        return -1;
+    }
+
+    if (!PyLong_Check(obj)) {
+        Py_DECREF(obj);
+        PyErr_Format(PyExc_TypeError, "config option %s is not an int", name);
+        return -1;
+    }
+
+    int as_int = PyLong_AsInt(obj);
+    Py_DECREF(obj);
+    if (as_int == -1 && PyErr_Occurred()) {
+        PyErr_Format(PyExc_OverflowError,
+                     "config option %s value does not fit into a C int", name);
+        return -1;
+    }
+
+    *value = as_int;
+    return 0;
+}
+
+
+static int
+config_names_add(PyObject *names, const PyConfigSpec *spec)
+{
+    for (; spec->name != NULL; spec++) {
+        if (spec->visibility == PyConfig_MEMBER_INIT_ONLY) {
+            continue;
+        }
+        PyObject *name = PyUnicode_FromString(spec->name);
+        if (name == NULL) {
+            return -1;
+        }
+        int res = PyList_Append(names, name);
+        Py_DECREF(name);
+        if (res < 0) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
+
+PyObject*
+PyConfig_Names(void)
+{
+    PyObject *names = PyList_New(0);
+    if (names == NULL) {
+        goto error;
+    }
+
+    if (config_names_add(names, PYCONFIG_SPEC) < 0) {
+        goto error;
+    }
+    if (config_names_add(names, PYPRECONFIG_SPEC) < 0) {
+        goto error;
+    }
+
+    PyObject *frozen = PyFrozenSet_New(names);
+    Py_DECREF(names);
+    return frozen;
+
+error:
+    Py_XDECREF(names);
+    return NULL;
+}
+
+
+// --- PyConfig_Set() -------------------------------------------------------
+
+static int
+config_set_sys_flag(const PyConfigSpec *spec, int int_value)
+{
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    PyConfig *config = &interp->config;
+
+    if (spec->type == PyConfig_MEMBER_BOOL) {
+        if (int_value != 0) {
+            // convert values < 0 and values > 1 to 1
+            int_value = 1;
+        }
+    }
+
+    PyObject *value;
+    if (spec->sys.flag_setter) {
+        value = spec->sys.flag_setter(int_value);
+    }
+    else {
+        value = config_sys_flag_long(int_value);
+    }
+    if (value == NULL) {
+        return -1;
+    }
+
+    // Set sys.flags.FLAG
+    Py_ssize_t pos = spec->sys.flag_index;
+    if (_PySys_SetFlagObj(pos, value) < 0) {
+        goto error;
+    }
+
+    // Set PyConfig.ATTR
+    assert(spec->type == PyConfig_MEMBER_INT
+           || spec->type == PyConfig_MEMBER_UINT
+           || spec->type == PyConfig_MEMBER_BOOL);
+    int *member = config_spec_get_member(spec, config);
+    *member = int_value;
+
+    // Set sys.dont_write_bytecode attribute
+    if (strcmp(spec->name, "write_bytecode") == 0) {
+        if (PySys_SetObject("dont_write_bytecode", value) < 0) {
+            goto error;
+        }
+    }
+
+    Py_DECREF(value);
+    return 0;
+
+error:
+    Py_DECREF(value);
+    return -1;
+}
+
+
+int
+PyConfig_Set(const char *name, PyObject *value)
+{
+    const PyConfigSpec *spec = config_find_spec(name);
+    if (spec == NULL) {
+        spec = preconfig_find_spec(name);
+        if (spec == NULL) {
+            config_unknown_name_error(name);
+            return -1;
+        }
+        assert(spec->visibility != PyConfig_MEMBER_PUBLIC);
+    }
+
+    if (spec->visibility != PyConfig_MEMBER_PUBLIC) {
+        PyErr_Format(PyExc_ValueError, "cannot set read-only option %s",
+                     name);
+        return -1;
+    }
+
+    int int_value = 0;
+    int has_int_value = 0;
+
+    switch (spec->type) {
+    case PyConfig_MEMBER_INT:
+    case PyConfig_MEMBER_UINT:
+    case PyConfig_MEMBER_BOOL:
+        if (!PyLong_Check(value)) {
+            PyErr_Format(PyExc_TypeError, "expected int or bool, got %T", value);
+            return -1;
+        }
+        int_value = PyLong_AsInt(value);
+        if (int_value == -1 && PyErr_Occurred()) {
+            return -1;
+        }
+        if (int_value < 0 && spec->type != PyConfig_MEMBER_INT) {
+            PyErr_Format(PyExc_ValueError, "value must be >= 0");
+            return -1;
+        }
+        has_int_value = 1;
+        break;
+
+    case PyConfig_MEMBER_ULONG:
+        // not implemented: only hash_seed uses this type, and it's read-only
+        goto cannot_set;
+
+    case PyConfig_MEMBER_WSTR:
+        if (!PyUnicode_CheckExact(value)) {
+            PyErr_Format(PyExc_TypeError, "expected str, got %T", value);
+            return -1;
+        }
+        break;
+
+    case PyConfig_MEMBER_WSTR_OPT:
+        if (value != Py_None && !PyUnicode_CheckExact(value)) {
+            PyErr_Format(PyExc_TypeError, "expected str or None, got %T", value);
+            return -1;
+        }
+        break;
+
+    case PyConfig_MEMBER_WSTR_LIST:
+        if (strcmp(spec->name, "xoptions") != 0) {
+            if (!PyList_Check(value)) {
+                PyErr_Format(PyExc_TypeError, "expected list[str], got %T",
+                             value);
+                return -1;
+            }
+            for (Py_ssize_t i=0; i < PyList_GET_SIZE(value); i++) {
+                PyObject *item = PyList_GET_ITEM(value, i);
+                if (!PyUnicode_Check(item)) {
+                    PyErr_Format(PyExc_TypeError,
+                                 "expected str, list item %zd has type %T",
+                                 i, item);
+                    return -1;
+                }
+            }
+        }
+        else {
+            // xoptions type is dict[str, str]
+            if (!PyDict_Check(value)) {
+                PyErr_Format(PyExc_TypeError,
+                             "expected dict[str, str | bool], got %T",
+                             value);
+                return -1;
+            }
+
+            Py_ssize_t pos = 0;
+            PyObject *key, *item;
+            while (PyDict_Next(value, &pos, &key, &item)) {
+                if (!PyUnicode_Check(key)) {
+                    PyErr_Format(PyExc_TypeError,
+                                 "expected str, "
+                                 "got dict key type %T", key);
+                    return -1;
+                }
+                if (!PyUnicode_Check(item) && !PyBool_Check(item)) {
+                    PyErr_Format(PyExc_TypeError,
+                                 "expected str or bool, "
+                                 "got dict value type %T", key);
+                    return -1;
+                }
+            }
+        }
+        break;
+
+    default:
+        Py_UNREACHABLE();
+    }
+
+
+    if (spec->sys.attr != NULL) {
+        // Set the sys attribute, but don't set PyInterpreterState.config
+        // to keep the code simple.
+        return PySys_SetObject(spec->sys.attr, value);
+    }
+    else if (spec->sys.flag_index >= 0 && has_int_value) {
+        return config_set_sys_flag(spec, int_value);
+    }
+    else if (strcmp(spec->name, "int_max_str_digits") == 0 && has_int_value) {
+        return _PySys_SetIntMaxStrDigits(int_value);
+    }
+
+cannot_set:
+    PyErr_Format(PyExc_ValueError, "cannot set option %s", name);
+    return -1;
+}
index 1fff7e417673987a35eb54d9cb432ed56d5c7f81..887a916563a2e1a76ce5f90885dc2d6b440587d7 100644 (file)
@@ -1845,6 +1845,7 @@ sys_get_int_max_str_digits_impl(PyObject *module)
     return PyLong_FromLong(interp->long_state.max_str_digits);
 }
 
+
 /*[clinic input]
 sys.set_int_max_str_digits
 
@@ -1857,16 +1858,10 @@ static PyObject *
 sys_set_int_max_str_digits_impl(PyObject *module, int maxdigits)
 /*[clinic end generated code: output=734d4c2511f2a56d input=d7e3f325db6910c5]*/
 {
-    PyThreadState *tstate = _PyThreadState_GET();
-    if ((!maxdigits) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD)) {
-        tstate->interp->long_state.max_str_digits = maxdigits;
-        Py_RETURN_NONE;
-    } else {
-        PyErr_Format(
-            PyExc_ValueError, "maxdigits must be 0 or larger than %d",
-            _PY_LONG_MAX_STR_DIGITS_THRESHOLD);
+    if (_PySys_SetIntMaxStrDigits(maxdigits) < 0) {
         return NULL;
     }
+    Py_RETURN_NONE;
 }
 
 size_t
@@ -3120,6 +3115,8 @@ static PyStructSequence_Field flags_fields[] = {
     {0}
 };
 
+#define SYS_FLAGS_INT_MAX_STR_DIGITS 17
+
 static PyStructSequence_Desc flags_desc = {
     "sys.flags",        /* name */
     flags__doc__,       /* doc */
@@ -3127,6 +3124,48 @@ static PyStructSequence_Desc flags_desc = {
     18
 };
 
+static void
+sys_set_flag(PyObject *flags, Py_ssize_t pos, PyObject *value)
+{
+    assert(pos >= 0 && pos < (Py_ssize_t)(Py_ARRAY_LENGTH(flags_fields) - 1));
+
+    PyObject *old_value = PyStructSequence_GET_ITEM(flags, pos);
+    PyStructSequence_SET_ITEM(flags, pos, Py_NewRef(value));
+    Py_XDECREF(old_value);
+}
+
+
+int
+_PySys_SetFlagObj(Py_ssize_t pos, PyObject *value)
+{
+    PyObject *flags = Py_XNewRef(PySys_GetObject("flags"));
+    if (flags == NULL) {
+        if (!PyErr_Occurred()) {
+            PyErr_SetString(PyExc_RuntimeError, "lost sys.flags");
+        }
+        return -1;
+    }
+
+    sys_set_flag(flags, pos, value);
+    Py_DECREF(flags);
+    return 0;
+}
+
+
+static int
+_PySys_SetFlagInt(Py_ssize_t pos, int value)
+{
+    PyObject *obj = PyLong_FromLong(value);
+    if (obj == NULL) {
+        return -1;
+    }
+
+    int res = _PySys_SetFlagObj(pos, obj);
+    Py_DECREF(obj);
+    return res;
+}
+
+
 static int
 set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
 {
@@ -3142,8 +3181,8 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
         if (value == NULL) { \
             return -1; \
         } \
-        Py_XDECREF(PyStructSequence_GET_ITEM(flags, pos)); \
-        PyStructSequence_SET_ITEM(flags, pos, value); \
+        sys_set_flag(flags, pos, value); \
+        Py_DECREF(value); \
         pos++; \
     } while (0)
 #define SetFlag(expr) SetFlagObj(PyLong_FromLong(expr))
@@ -3599,64 +3638,6 @@ err_occurred:
     return _PyStatus_ERR("can't initialize sys module");
 }
 
-static int
-sys_add_xoption(PyObject *opts, const wchar_t *s)
-{
-    PyObject *name, *value = NULL;
-
-    const wchar_t *name_end = wcschr(s, L'=');
-    if (!name_end) {
-        name = PyUnicode_FromWideChar(s, -1);
-        if (name == NULL) {
-            goto error;
-        }
-        value = Py_NewRef(Py_True);
-    }
-    else {
-        name = PyUnicode_FromWideChar(s, name_end - s);
-        if (name == NULL) {
-            goto error;
-        }
-        value = PyUnicode_FromWideChar(name_end + 1, -1);
-        if (value == NULL) {
-            goto error;
-        }
-    }
-    if (PyDict_SetItem(opts, name, value) < 0) {
-        goto error;
-    }
-    Py_DECREF(name);
-    Py_DECREF(value);
-    return 0;
-
-error:
-    Py_XDECREF(name);
-    Py_XDECREF(value);
-    return -1;
-}
-
-
-static PyObject*
-sys_create_xoptions_dict(const PyConfig *config)
-{
-    Py_ssize_t nxoption = config->xoptions.length;
-    wchar_t * const * xoptions = config->xoptions.items;
-    PyObject *dict = PyDict_New();
-    if (dict == NULL) {
-        return NULL;
-    }
-
-    for (Py_ssize_t i=0; i < nxoption; i++) {
-        const wchar_t *option = xoptions[i];
-        if (sys_add_xoption(dict, option) < 0) {
-            Py_DECREF(dict);
-            return NULL;
-        }
-    }
-
-    return dict;
-}
-
 
 // Update sys attributes for a new PyConfig configuration.
 // This function also adds attributes that _PySys_InitCore() didn't add.
@@ -3703,7 +3684,7 @@ _PySys_UpdateConfig(PyThreadState *tstate)
     COPY_LIST("orig_argv", config->orig_argv);
     COPY_LIST("warnoptions", config->warnoptions);
 
-    SET_SYS("_xoptions", sys_create_xoptions_dict(config));
+    SET_SYS("_xoptions", _PyConfig_CreateXOptionsDict(config));
 
     const wchar_t *stdlibdir = _Py_GetStdlibDir();
     if (stdlibdir != NULL) {
@@ -4129,3 +4110,28 @@ PySys_FormatStderr(const char *format, ...)
     sys_format(&_Py_ID(stderr), stderr, format, va);
     va_end(va);
 }
+
+
+int
+_PySys_SetIntMaxStrDigits(int maxdigits)
+{
+    if (maxdigits != 0 && maxdigits < _PY_LONG_MAX_STR_DIGITS_THRESHOLD) {
+        PyErr_Format(
+            PyExc_ValueError, "maxdigits must be 0 or larger than %d",
+            _PY_LONG_MAX_STR_DIGITS_THRESHOLD);
+        return -1;
+    }
+
+    // Set sys.flags.int_max_str_digits
+    const Py_ssize_t pos = SYS_FLAGS_INT_MAX_STR_DIGITS;
+    if (_PySys_SetFlagInt(pos, maxdigits) < 0) {
+        return -1;
+    }
+
+    // Set PyInterpreterState.long_state.max_str_digits
+    // and PyInterpreterState.config.int_max_str_digits.
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    interp->long_state.max_str_digits = maxdigits;
+    interp->config.int_max_str_digits = maxdigits;
+    return 0;
+}
index 4b75dca86ed2150ab12c8223b83bf3271ea31c7b..3a73f65f8ff7b3bbc6533bfa609e9b6f4ffd285d 100644 (file)
@@ -329,6 +329,7 @@ MAX_SIZES = {
     _abs('Python/parking_lot.c'): (40_000, 1000),
     _abs('Python/pylifecycle.c'): (500_000, 5000),
     _abs('Python/pystate.c'): (500_000, 5000),
+    _abs('Python/initconfig.c'): (50_000, 500),
 
     # Generated files:
     _abs('Include/internal/pycore_opcode.h'): (10_000, 1000),
index b91b11763fb56c6d70a56e1ba504558c35ca0e59..bce64ed0ed609a3ab26d4bfeaecba2057a50de41 100644 (file)
@@ -90,6 +90,7 @@ Python/initconfig.c   -       _Py_StandardStreamErrors        -
 
 # Internal constant list
 Python/initconfig.c    -       PYCONFIG_SPEC   -
+Python/initconfig.c    -       PYPRECONFIG_SPEC        -
 
 
 ##-----------------------