""" 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", int, 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()