Skip to content

Commit

Permalink
pythongh-107954: Add PyConfig_Get() function
Browse files Browse the repository at this point in the history
Add PyConfig_Get() and PyConfig_GetInt() functions to get the current
Python configuration.

_PyConfig_AsDict() now converts PyConfig.xoptions as a dictionary.
  • Loading branch information
vstinner committed Dec 1, 2023
1 parent 5c5022b commit 36dcd34
Show file tree
Hide file tree
Showing 14 changed files with 653 additions and 193 deletions.
47 changes: 47 additions & 0 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,53 @@ customized Python always running in isolated mode using
:c:func:`Py_RunMain`.
Get the current Python configuration
====================================
Get a configuration option where *name* is the name of a :c:type:`PyConfig`
member.
Some options are read from the :mod:`sys` attributes. For example, the option
``"argv"`` is read from :data:`sys.argv`.
.. c:function:: int PyConfig_Get(const char *name, PyObject **value)
Get a configuration option as a Python object.
The object type depends on the configuration option. It can be:
* ``int``
* ``str``
* ``list[str]``
* ``dict[str, str]``
* Return ``0`` and set *\*value* on success.
* Raise an exception and return ``-1`` on error.
.. c:function:: int PyConfig_GetInt(const char *name, int *value)
Similar to :c:func:`PyConfig_Get`, but get the value as a C int.
Example
-------
Code::
int get_verbose(void)
{
int verbose;
if (PyConfig_GetInt("verbose", &verbose) < 0) {
// Silently ignore the error
PyErr_Clear();
return -1;
}
return verbose;
}
Py_GetArgcArgv()
================
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,12 @@ New Features
:exc:`KeyError` if the key missing.
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)

* Add functions to get the current Python configuration:

* :c:func:`PyConfig_Get`
* :c:func:`PyConfig_GetInt`

(Contributed by Victor Stinner in :gh:`107954`.)

Porting to Python 3.13
----------------------
Expand Down Expand Up @@ -1498,6 +1504,9 @@ Pending Removal in Python 3.14
* :c:var:`!Py_FileSystemDefaultEncodeErrors`: use :c:member:`PyConfig.filesystem_errors`
* :c:var:`!Py_UTF8Mode`: use :c:member:`PyPreConfig.utf8_mode` (see :c:func:`Py_PreInitialize`)

Use :c:func:`PyConfig_GetInt` and :c:func:`PyConfig_Get` functions to get
these configuration options.

The :c:func:`Py_InitializeFromConfig` API should be used with
:c:type:`PyConfig` instead.

Expand Down
18 changes: 18 additions & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,24 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config,
Py_ssize_t length, wchar_t **items);


/* --- PyConfig_Get() ----------------------------------------- */

// Get 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:
// int, str, list[str] and dict[str, str].
PyAPI_FUNC(PyObject*) PyConfig_Get(const char *name);

// Get an configuration option as an integer.
// Return 0 and set '*value' on success.
// Raise an exception return -1 on error.
PyAPI_FUNC(int) PyConfig_GetInt(
const char *name,
int *value);


/* --- Helper functions --------------------------------------- */

/* Get the original command line arguments, before Python modified them.
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,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);

Expand Down
7 changes: 4 additions & 3 deletions Lib/test/_test_embed_set_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ def test_set_invalid(self):
'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]))
Expand Down Expand Up @@ -201,9 +202,9 @@ def test_options(self):
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):
Expand Down
105 changes: 105 additions & 0 deletions Lib/test/test_capi/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
Tests on PyConfig API (PEP 587).
"""
import os
import sys
import unittest
from test import support
try:
import _testcapi
except ImportError:
_testcapi = None


@unittest.skipIf(_testcapi is None, 'need _testcapi')
class CAPITests(unittest.TestCase):
def check_config_get(self, get_func):
# write_bytecode is read from sys.dont_write_bytecode as int
with support.swap_attr(sys, "dont_write_bytecode", 0):
self.assertEqual(get_func('write_bytecode'), 1)
with support.swap_attr(sys, "dont_write_bytecode", "yes"):
self.assertEqual(get_func('write_bytecode'), 0)
with support.swap_attr(sys, "dont_write_bytecode", []):
self.assertEqual(get_func('write_bytecode'), 1)

# non-existent config option name
NONEXISTENT_KEY = 'NONEXISTENT_KEY'
err_msg = f'unknown config option name: {NONEXISTENT_KEY}'
with self.assertRaisesRegex(ValueError, err_msg):
get_func('NONEXISTENT_KEY')

def test_config_get(self):
config_get = _testcapi.config_get

self.check_config_get(config_get)

for name, config_type, expected in (
('verbose', int, sys.flags.verbose), # PyConfig_MEMBER_INT
('isolated', int, sys.flags.isolated), # PyConfig_MEMBER_UINT
('platlibdir', str, sys.platlibdir), # PyConfig_MEMBER_WSTR
('argv', list, sys.argv), # PyConfig_MEMBER_WSTR_LIST
('xoptions', dict, sys._xoptions), # xoptions dict
):
with self.subTest(name=name):
value = config_get(name)
self.assertEqual(type(value), config_type)
self.assertEqual(value, expected)

# PyConfig_MEMBER_ULONG type
hash_seed = config_get('hash_seed')
self.assertIsInstance(hash_seed, int)
self.assertGreaterEqual(hash_seed, 0)

# PyConfig_MEMBER_WSTR_OPT type
if 'PYTHONDUMPREFSFILE' not in os.environ:
self.assertIsNone(config_get('dump_refs_file'))

# attributes read from sys
value_str = "TEST_MARKER_STR"
value_list = ["TEST_MARKER_STRLIST"]
value_dict = {"x": "value", "y": True}
for name, sys_name, value in (
("base_exec_prefix", None, value_str),
("base_prefix", None, value_str),
("exec_prefix", None, value_str),
("executable", None, value_str),
("platlibdir", None, value_str),
("prefix", None, value_str),
("pycache_prefix", None, value_str),
("base_executable", "_base_executable", value_str),
("stdlib_dir", "_stdlib_dir", value_str),
("argv", None, value_list),
("orig_argv", None, value_list),
("warnoptions", None, value_list),
("module_search_paths", "path", value_list),
("xoptions", "_xoptions", value_dict),
):
with self.subTest(name=name):
if sys_name is None:
sys_name = name
with support.swap_attr(sys, sys_name, value):
self.assertEqual(config_get(name), value)

def test_config_getint(self):
config_getint = _testcapi.config_getint

self.check_config_get(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
hash_seed = config_getint('hash_seed')
self.assertIsInstance(hash_seed, int)
self.assertGreaterEqual(hash_seed, 0)

# platlibdir is a str
with self.assertRaises(TypeError):
config_getint('platlibdir')


if __name__ == "__main__":
unittest.main()
36 changes: 20 additions & 16 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'argv': [""],
'orig_argv': [],

'xoptions': [],
'xoptions': {},
'warnoptions': [],

'pythonpath_env': None,
Expand Down Expand Up @@ -509,7 +509,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'check_hash_pycs_mode': 'default',
'pathconfig_warnings': 1,
'_init_main': 1,
'use_frozen_modules': not support.Py_DEBUG,
'use_frozen_modules': int(not support.Py_DEBUG),
'safe_path': 0,
'_is_python_build': IGNORE_CONFIG,
}
Expand Down Expand Up @@ -867,12 +867,12 @@ def test_init_from_config(self):
'-c', 'pass',
'arg2'],
'parse_argv': 2,
'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',
Expand Down Expand Up @@ -1016,7 +1016,7 @@ def test_preinit_parse_argv(self):
'dev_mode': 1,
'faulthandler': 1,
'warnoptions': ['default'],
'xoptions': ['dev'],
'xoptions': {'dev': True},
'safe_path': 1,
}
self.check_all_configs("test_preinit_parse_argv", config, preconfig,
Expand Down Expand Up @@ -1108,12 +1108,12 @@ def modify_path(path):
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',
Expand Down Expand Up @@ -1654,14 +1654,18 @@ def test_init_use_frozen_modules(self):
}
for raw, expected in tests:
optval = f'frozen_modules{raw}'
if raw.startswith('='):
xoption_value = raw[1:]
else:
xoption_value = True
config = {
'parse_argv': 2,
'argv': ['-c'],
'orig_argv': ['./argv0', '-X', optval, '-c', 'pass'],
'program_name': './argv0',
'run_command': 'pass\n',
'use_environment': 1,
'xoptions': [optval],
'xoptions': {'frozen_modules': xoption_value},
'use_frozen_modules': expected,
}
env = {'TESTFROZEN': raw[1:]} if raw else None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Add functions to get the current Python configuration:

* :c:func:`PyConfig_Get`
* :c:func:`PyConfig_GetInt`

Patch by Victor Stinner.
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
@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/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/bytearray.c _testcapi/bytes.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/pyos.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/sys.c _testcapi/hash.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/bytearray.c _testcapi/bytes.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/pyos.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/sys.c _testcapi/hash.c _testcapi/config.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Expand Down
45 changes: 45 additions & 0 deletions Modules/_testcapi/config.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#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 PyMethodDef test_methods[] = {
{"config_get", _testcapi_config_get, METH_O},
{"config_getint", _testcapi_config_getint, METH_O},
{NULL}
};

int _PyTestCapi_Init_Config(PyObject *mod)
{
if (PyModule_AddFunctions(mod, test_methods) < 0) {
return -1;
}

return 0;
}
1 change: 1 addition & 0 deletions Modules/_testcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ int _PyTestCapi_Init_Hash(PyObject *module);

int _PyTestCapi_Init_VectorcallLimited(PyObject *module);
int _PyTestCapi_Init_HeaptypeRelative(PyObject *module);
int _PyTestCapi_Init_Config(PyObject *mod);

#endif // Py_TESTCAPI_PARTS_H
Loading

0 comments on commit 36dcd34

Please sign in to comment.