cpython/Lib/test/test_capi/test_config.py

380 lines
15 KiB
Python

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