bpo-42260: Add _PyConfig_FromDict() (GH-23167)

* Rename config_as_dict() to _PyConfig_AsDict().
* Add 'module_search_paths_set' to _PyConfig_AsDict().
* Add _PyConfig_FromDict().
* Add get_config() and set_config() to _testinternalcapi.
* Add config_check_consistency().
diff --git a/Lib/test/_test_embed_set_config.py b/Lib/test/_test_embed_set_config.py
new file mode 100644
index 0000000..7c91381
--- /dev/null
+++ b/Lib/test/_test_embed_set_config.py
@@ -0,0 +1,243 @@
+# bpo-42260: Test _PyInterpreterState_GetConfigCopy()
+# and _PyInterpreterState_SetConfig().
+#
+# Test run in a subinterpreter since set_config(get_config())
+# does reset sys attributes to their state of the Python startup
+# (before the site module is run).
+
+import _testinternalcapi
+import os
+import sys
+import unittest
+
+
+MS_WINDOWS = (os.name == 'nt')
+MAX_HASH_SEED = 4294967295
+
+class SetConfigTests(unittest.TestCase):
+    def setUp(self):
+        self.old_config = _testinternalcapi.get_config()
+        self.sys_copy = dict(sys.__dict__)
+
+    def tearDown(self):
+        self.set_config(parse_argv=0)
+        sys.__dict__.clear()
+        sys.__dict__.update(self.sys_copy)
+
+    def set_config(self, **kwargs):
+        _testinternalcapi.set_config(self.old_config | kwargs)
+
+    def check(self, **kwargs):
+        self.set_config(**kwargs)
+        for key, value in kwargs.items():
+            self.assertEqual(getattr(sys, key), value,
+                             (key, value))
+
+    def test_set_invalid(self):
+        invalid_uint = -1
+        NULL = None
+        invalid_wstr = NULL
+        # PyWideStringList strings must be non-NULL
+        invalid_wstrlist = ["abc", NULL, "def"]
+
+        type_tests = []
+        value_tests = [
+            # enum
+            ('_config_init', 0),
+            ('_config_init', 4),
+            # unsigned long
+            ("hash_seed", -1),
+            ("hash_seed", MAX_HASH_SEED + 1),
+        ]
+
+        # int (unsigned)
+        options = [
+            '_config_init',
+            'isolated',
+            'use_environment',
+            'dev_mode',
+            'install_signal_handlers',
+            'use_hash_seed',
+            'faulthandler',
+            'tracemalloc',
+            'import_time',
+            'show_ref_count',
+            'dump_refs',
+            'malloc_stats',
+            'parse_argv',
+            'site_import',
+            'bytes_warning',
+            'inspect',
+            'interactive',
+            'optimization_level',
+            'parser_debug',
+            'write_bytecode',
+            'verbose',
+            'quiet',
+            'user_site_directory',
+            'configure_c_stdio',
+            'buffered_stdio',
+            'pathconfig_warnings',
+            'module_search_paths_set',
+            'skip_source_first_line',
+            '_install_importlib',
+            '_init_main',
+            '_isolated_interpreter',
+        ]
+        if MS_WINDOWS:
+            options.append('legacy_windows_stdio')
+        for key in options:
+            value_tests.append((key, invalid_uint))
+            type_tests.append((key, "abc"))
+            type_tests.append((key, 2.0))
+
+        # wchar_t*
+        for key in (
+            'filesystem_encoding',
+            'filesystem_errors',
+            'stdio_encoding',
+            'stdio_errors',
+            'check_hash_pycs_mode',
+            'program_name',
+            'platlibdir',
+            'executable',
+            'base_executable',
+            'prefix',
+            'base_prefix',
+            'exec_prefix',
+            'base_exec_prefix',
+            # optional wstr:
+            # 'pythonpath_env'
+            # 'home',
+            # 'pycache_prefix'
+            # 'run_command'
+            # 'run_module'
+            # 'run_filename'
+        ):
+            value_tests.append((key, invalid_wstr))
+            type_tests.append((key, b'bytes'))
+            type_tests.append((key, 123))
+
+        # PyWideStringList
+        for key in (
+            'orig_argv',
+            'argv',
+            'xoptions',
+            'warnoptions',
+            'module_search_paths',
+        ):
+            value_tests.append((key, invalid_wstrlist))
+            type_tests.append((key, 123))
+            type_tests.append((key, "abc"))
+            type_tests.append((key, [123]))
+            type_tests.append((key, [b"bytes"]))
+
+
+        if MS_WINDOWS:
+            value_tests.append(('legacy_windows_stdio', invalid_uint))
+
+        for exc_type, tests in (
+            (ValueError, value_tests),
+            (TypeError, type_tests),
+        ):
+            for key, value in tests:
+                config = self.old_config | {key: value}
+                with self.subTest(key=key, value=value, exc_type=exc_type):
+                    with self.assertRaises(exc_type):
+                        _testinternalcapi.set_config(config)
+
+    def test_flags(self):
+        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),
+        ):
+            with self.subTest(sys=sys_attr, key=key, value=value):
+                self.set_config(**{key: value, 'parse_argv': 0})
+                self.assertEqual(getattr(sys.flags, sys_attr), value)
+
+        self.set_config(write_bytecode=0)
+        self.assertEqual(sys.flags.dont_write_bytecode, True)
+        self.assertEqual(sys.dont_write_bytecode, True)
+
+        self.set_config(write_bytecode=1)
+        self.assertEqual(sys.flags.dont_write_bytecode, False)
+        self.assertEqual(sys.dont_write_bytecode, False)
+
+        self.set_config(user_site_directory=0, isolated=0)
+        self.assertEqual(sys.flags.no_user_site, 1)
+        self.set_config(user_site_directory=1, isolated=0)
+        self.assertEqual(sys.flags.no_user_site, 0)
+
+        self.set_config(site_import=0)
+        self.assertEqual(sys.flags.no_site, 1)
+        self.set_config(site_import=1)
+        self.assertEqual(sys.flags.no_site, 0)
+
+        self.set_config(dev_mode=0)
+        self.assertEqual(sys.flags.dev_mode, False)
+        self.set_config(dev_mode=1)
+        self.assertEqual(sys.flags.dev_mode, True)
+
+        self.set_config(use_environment=0, isolated=0)
+        self.assertEqual(sys.flags.ignore_environment, 1)
+        self.set_config(use_environment=1, isolated=0)
+        self.assertEqual(sys.flags.ignore_environment, 0)
+
+        self.set_config(use_hash_seed=1, hash_seed=0)
+        self.assertEqual(sys.flags.hash_randomization, 0)
+        self.set_config(use_hash_seed=0, hash_seed=0)
+        self.assertEqual(sys.flags.hash_randomization, 1)
+        self.set_config(use_hash_seed=1, hash_seed=123)
+        self.assertEqual(sys.flags.hash_randomization, 1)
+
+    def test_options(self):
+        self.check(warnoptions=[])
+        self.check(warnoptions=["default", "ignore"])
+
+        self.set_config(xoptions=[])
+        self.assertEqual(sys._xoptions, {})
+        self.set_config(xoptions=["dev", "tracemalloc=5"])
+        self.assertEqual(sys._xoptions, {"dev": True, "tracemalloc": "5"})
+
+    def test_pathconfig(self):
+        self.check(
+            executable='executable',
+            prefix="prefix",
+            base_prefix="base_prefix",
+            exec_prefix="exec_prefix",
+            base_exec_prefix="base_exec_prefix",
+            platlibdir="platlibdir")
+
+        self.set_config(base_executable="base_executable")
+        self.assertEqual(sys._base_executable, "base_executable")
+
+    def test_path(self):
+        self.set_config(module_search_paths_set=1,
+                        module_search_paths=['a', 'b', 'c'])
+        self.assertEqual(sys.path, ['a', 'b', 'c'])
+
+        # Leave sys.path unchanged if module_search_paths_set=0
+        self.set_config(module_search_paths_set=0,
+                        module_search_paths=['new_path'])
+        self.assertEqual(sys.path, ['a', 'b', 'c'])
+
+    def test_argv(self):
+        self.set_config(parse_argv=0,
+                        argv=['python_program', 'args'],
+                        orig_argv=['orig', 'orig_args'])
+        self.assertEqual(sys.argv, ['python_program', 'args'])
+        self.assertEqual(sys.orig_argv, ['orig', 'orig_args'])
+
+    def test_pycache_prefix(self):
+        self.check(pycache_prefix=None)
+        self.check(pycache_prefix="pycache_prefix")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index 36a0e77..9182061 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -30,6 +30,8 @@
 # _PyCoreConfig_InitIsolatedConfig()
 API_ISOLATED = 3
 
+MAX_HASH_SEED = 4294967295
+
 
 def debug_build(program):
     program = os.path.basename(program)
@@ -382,6 +384,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         'exec_prefix': GET_DEFAULT_CONFIG,
         'base_exec_prefix': GET_DEFAULT_CONFIG,
         'module_search_paths': GET_DEFAULT_CONFIG,
+        'module_search_paths_set': 1,
         'platlibdir': sys.platlibdir,
 
         'site_import': 1,
@@ -1408,6 +1411,17 @@ def test_get_argc_argv(self):
         # ignore output
 
 
+class SetConfigTests(unittest.TestCase):
+    def test_set_config(self):
+        # bpo-42260: Test _PyInterpreterState_SetConfig()
+        cmd = [sys.executable, '-I', '-m', 'test._test_embed_set_config']
+        proc = subprocess.run(cmd,
+                              stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE)
+        self.assertEqual(proc.returncode, 0,
+                         (proc.returncode, proc.stdout, proc.stderr))
+
+
 class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
     def test_open_code_hook(self):
         self.run_embedded_interpreter("test_open_code_hook")