Skip to content

[PEP 741] gh-107954: Add PyConfig_Get() function #112609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,62 @@ 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:: PyObject* PyConfig_Get(const char *name)

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 value:

* Return a new reference on success.
* Set an exception and return ``NULL`` on error.

The function must not be called before Python is initialized or after
Python is finalized. The caller must hold the GIL.

.. versionadded:: 3.13


.. c:function:: int PyConfig_GetInt(const char *name, int *value)

Similar to :c:func:`PyConfig_Get`, but get the value as a C int.

.. versionadded:: 3.13


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;
}
Loading