Skip to content
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

gh-96512: Move int_max_str_digits setting to PyConfig #96944

Merged
merged 9 commits into from
Oct 3, 2022
18 changes: 18 additions & 0 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,24 @@ PyConfig

Default: ``0``.

.. c:member:: int int_max_str_digits

Configures the :ref:`integer string conversion length limitation
<int_max_str_digits>`. An initial value of ``-1`` means the value will
be taken from the command line or environment or otherwise default to
4300 (:data:`sys.int_info.default_max_str_digits`). A value of ``0``
disables the limitation. Values greater than zero but less than 640
(:data:`sys.int_info.str_digits_check_threshold`) are unsupported and
will produce an error.

gpshead marked this conversation as resolved.
Show resolved Hide resolved
Configured by the :option:`-X int_max_str_digits <-X>` command line
flag or the :envvar:`PYTHONINTMAXSTRDIGITS` environment varable.

Default: ``-1`` in Python mode. 4300
(:data:`sys.int_info.default_max_str_digits`) in isolated mode.

.. versionadded:: 3.12

.. c:member:: int isolated

If greater than ``0``, enable isolated mode:
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ typedef struct PyConfig {
wchar_t *check_hash_pycs_mode;
int use_frozen_modules;
int safe_path;
int int_max_str_digits;

/* --- Path configuration inputs ------------ */
int pathconfig_warnings;
Expand Down
2 changes: 0 additions & 2 deletions Include/internal/pycore_initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,6 @@ extern void _Py_DumpPathConfig(PyThreadState *tstate);

PyAPI_FUNC(PyObject*) _Py_Get_Getpath_CodeObject(void);

extern int _Py_global_config_int_max_str_digits; // TODO(gpshead): move this into PyConfig in 3.12 after the backports ship.


/* --- Function used for testing ---------------------------------- */

Expand Down
2 changes: 0 additions & 2 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,6 @@ struct _is {
struct types_state types;
struct callable_cache callable_cache;

int int_max_str_digits;

/* The following fields are here to avoid allocation during init.
The data is exposed through PyInterpreterState pointer fields.
These fields should not be accessed directly outside of init.
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,39 @@ async def foo(arg): return await arg # Py 3.5
self.assertEqual(ret, 0)
self.assertEqual(pickle.load(f), {'a': '123x', 'b': '123'})

def test_py_config_isoloated_per_interpreter(self):
# A config change in one interpreter must not leak to out to others.
#
# This test could verify ANY config value, it just happens to have been
# written around the time of int_max_str_digits. Refactoring is okay.
code = """if 1:
import sys, _testinternalcapi

# Any config value would do, this happens to be the one being
# double checked at the time this test was written.
config = _testinternalcapi.get_config()
config['int_max_str_digits'] = 55555
_testinternalcapi.set_config(config)
sub_value = _testinternalcapi.get_config()['int_max_str_digits']
assert sub_value == 55555, sub_value
"""
before_config = _testinternalcapi.get_config()
assert before_config['int_max_str_digits'] != 55555
gpshead marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(support.run_in_subinterp(code), 0,
'subinterp code failure, check stderr.')
after_config = _testinternalcapi.get_config()
self.assertIsNot(
before_config, after_config,
"Expected get_config() to return a new dict on each call")
self.assertEqual(before_config, after_config,
"CAUTION: Tests executed after this may be "
"running under an altered config.")
# try:...finally: calling set_config(before_config) not done
# as that results in sys.argv, sys.path, and sys.warnoptions
# "being modified by test_capi" per test.regrtest. So if this
# test fails, assume that the environment in this process may
# be altered and suspect.

def test_mutate_exception(self):
"""
Exceptions saved in global module state get shared between
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,8 @@ def res2int(res):
return tuple(int(i) for i in out.split())

res = assert_python_ok('-c', code)
self.assertEqual(res2int(res), (-1, sys.get_int_max_str_digits()))
current_max = sys.get_int_max_str_digits()
self.assertEqual(res2int(res), (current_max, current_max))
res = assert_python_ok('-X', 'int_max_str_digits=0', '-c', code)
self.assertEqual(res2int(res), (0, 0))
res = assert_python_ok('-X', 'int_max_str_digits=4000', '-c', code)
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'install_signal_handlers': 1,
'use_hash_seed': 0,
'hash_seed': 0,
'int_max_str_digits': sys.int_info.default_max_str_digits,
'faulthandler': 0,
'tracemalloc': 0,
'perf_profiling': 0,
Expand Down Expand Up @@ -876,6 +877,7 @@ def test_init_from_config(self):
'platlibdir': 'my_platlibdir',
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,
'int_max_str_digits': 31337,

'check_hash_pycs_mode': 'always',
'pathconfig_warnings': 0,
Expand Down Expand Up @@ -912,6 +914,7 @@ def test_init_compat_env(self):
'platlibdir': 'env_platlibdir',
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,
'int_max_str_digits': 4567,
}
self.check_all_configs("test_init_compat_env", config, preconfig,
api=API_COMPAT)
Expand Down Expand Up @@ -944,6 +947,7 @@ def test_init_python_env(self):
'platlibdir': 'env_platlibdir',
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,
'int_max_str_digits': 4567,
}
self.check_all_configs("test_init_python_env", config, preconfig,
api=API_PYTHON)
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,26 @@ def test_int_from_other_bases(self):
with self.subTest(base=base):
self._other_base_helper(base)

def test_int_max_str_digits_is_per_interpreter(self):
# Changing the limit in one interpreter does not change others.
code = """if 1:
# Subinterpreters maintain and enforce their own limit
import sys
sys.set_int_max_str_digits(2323)
try:
int('3'*3333)
except ValueError:
pass
else:
raise AssertionError('Expected a int max str digits ValueError.')
"""
with support.adjust_int_max_str_digits(4000):
before_value = sys.get_int_max_str_digits()
self.assertEqual(support.run_in_subinterp(code), 0,
'subinterp code failure, check stderr.')
after_value = sys.get_int_max_str_digits()
self.assertEqual(before_value, after_value)


class IntSubclassStrDigitLimitsTests(IntStrDigitLimitsTests):
int_class = IntSubclass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Configuration for the :ref:`integer string conversion length limitation
<int_max_str_digits>` now lives in the PyConfig C API struct.
10 changes: 3 additions & 7 deletions Objects/longobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1767,7 +1767,7 @@ long_to_decimal_string_internal(PyObject *aa,
if (size_a >= 10 * _PY_LONG_MAX_STR_DIGITS_THRESHOLD
/ (3 * PyLong_SHIFT) + 2) {
PyInterpreterState *interp = _PyInterpreterState_GET();
int max_str_digits = interp->int_max_str_digits;
int max_str_digits = interp->config.int_max_str_digits;
if ((max_str_digits > 0) &&
(max_str_digits / (3 * PyLong_SHIFT) <= (size_a - 11) / 10)) {
PyErr_Format(PyExc_ValueError, _MAX_STR_DIGITS_ERROR_FMT_TO_STR,
Expand Down Expand Up @@ -1837,7 +1837,7 @@ long_to_decimal_string_internal(PyObject *aa,
}
if (strlen > _PY_LONG_MAX_STR_DIGITS_THRESHOLD) {
PyInterpreterState *interp = _PyInterpreterState_GET();
int max_str_digits = interp->int_max_str_digits;
int max_str_digits = interp->config.int_max_str_digits;
Py_ssize_t strlen_nosign = strlen - negative;
if ((max_str_digits > 0) && (strlen_nosign > max_str_digits)) {
Py_DECREF(scratch);
Expand Down Expand Up @@ -2578,7 +2578,7 @@ long_from_string_base(const char **str, int base, PyLongObject **res)
* quadratic algorithm. */
if (digits > _PY_LONG_MAX_STR_DIGITS_THRESHOLD) {
PyInterpreterState *interp = _PyInterpreterState_GET();
int max_str_digits = interp->int_max_str_digits;
int max_str_digits = interp->config.int_max_str_digits;
if ((max_str_digits > 0) && (digits > max_str_digits)) {
PyErr_Format(PyExc_ValueError, _MAX_STR_DIGITS_ERROR_FMT_TO_INT,
max_str_digits, digits);
Expand Down Expand Up @@ -6235,10 +6235,6 @@ _PyLong_InitTypes(PyInterpreterState *interp)
return _PyStatus_ERR("can't init int info type");
}
}
interp->int_max_str_digits = _Py_global_config_int_max_str_digits;
if (interp->int_max_str_digits == -1) {
interp->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS;
}

return _PyStatus_OK();
}
Expand Down
4 changes: 4 additions & 0 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,9 @@ static int test_init_from_config(void)

config._isolated_interpreter = 1;

putenv("PYTHONINTMAXSTRDIGITS=6666");
gpshead marked this conversation as resolved.
Show resolved Hide resolved
config.int_max_str_digits = 31337;

init_from_config_clear(&config);

dump_config();
Expand Down Expand Up @@ -748,6 +751,7 @@ static void set_most_env_vars(void)
putenv("PYTHONIOENCODING=iso8859-1:replace");
putenv("PYTHONPLATLIBDIR=env_platlibdir");
putenv("PYTHONSAFEPATH=1");
putenv("PYTHONINTMAXSTRDIGITS=4567");
}


Expand Down
30 changes: 21 additions & 9 deletions Python/initconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ config_check_consistency(const PyConfig *config)
assert(config->pathconfig_warnings >= 0);
assert(config->_is_python_build >= 0);
assert(config->safe_path >= 0);
assert(config->int_max_str_digits >= 0);
// config->use_frozen_modules is initialized later
// by _PyConfig_InitImportConfig().
return 1;
Expand Down Expand Up @@ -789,14 +790,11 @@ _PyConfig_InitCompatConfig(PyConfig *config)
config->use_frozen_modules = 1;
#endif
config->safe_path = 0;
config->int_max_str_digits = -1;
config->_is_python_build = 0;
config->code_debug_ranges = 1;
}

/* Excluded from public struct PyConfig for backporting reasons. */
/* default to unconfigured, _PyLong_InitTypes() does the rest */
int _Py_global_config_int_max_str_digits = -1;


static void
config_init_defaults(PyConfig *config)
Expand Down Expand Up @@ -849,6 +847,7 @@ PyConfig_InitIsolatedConfig(PyConfig *config)
config->faulthandler = 0;
config->tracemalloc = 0;
config->perf_profiling = 0;
config->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS;
config->safe_path = 1;
config->pathconfig_warnings = 0;
#ifdef MS_WINDOWS
Expand Down Expand Up @@ -1021,6 +1020,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
COPY_ATTR(safe_path);
COPY_WSTRLIST(orig_argv);
COPY_ATTR(_is_python_build);
COPY_ATTR(int_max_str_digits);

#undef COPY_ATTR
#undef COPY_WSTR_ATTR
Expand Down Expand Up @@ -1128,6 +1128,7 @@ _PyConfig_AsDict(const PyConfig *config)
SET_ITEM_INT(use_frozen_modules);
SET_ITEM_INT(safe_path);
SET_ITEM_INT(_is_python_build);
SET_ITEM_INT(int_max_str_digits);

return dict;

Expand Down Expand Up @@ -1317,6 +1318,12 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
} \
CHECK_VALUE(#KEY, config->KEY >= 0); \
} while (0)
#define GET_INT(KEY) \
do { \
if (config_dict_get_int(dict, #KEY, &config->KEY) < 0) { \
return -1; \
} \
} while (0)
#define GET_WSTR(KEY) \
do { \
if (config_dict_get_wstr(dict, #KEY, config, &config->KEY) < 0) { \
Expand Down Expand Up @@ -1415,9 +1422,11 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
GET_UINT(use_frozen_modules);
GET_UINT(safe_path);
GET_UINT(_is_python_build);
GET_INT(int_max_str_digits);

#undef CHECK_VALUE
#undef GET_UINT
#undef GET_INT
#undef GET_WSTR
#undef GET_WSTR_OPT
return 0;
Expand Down Expand Up @@ -1782,7 +1791,7 @@ config_init_int_max_str_digits(PyConfig *config)

const char *env = config_get_env(config, "PYTHONINTMAXSTRDIGITS");
if (env) {
int valid = 0;
bool valid = 0;
if (!_Py_str_to_int(env, &maxdigits)) {
valid = ((maxdigits == 0) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD));
}
Expand All @@ -1794,13 +1803,13 @@ config_init_int_max_str_digits(PyConfig *config)
STRINGIFY(_PY_LONG_MAX_STR_DIGITS_THRESHOLD)
" or 0 for unlimited.");
}
_Py_global_config_int_max_str_digits = maxdigits;
config->int_max_str_digits = maxdigits;
}

const wchar_t *xoption = config_get_xoption(config, L"int_max_str_digits");
if (xoption) {
const wchar_t *sep = wcschr(xoption, L'=');
int valid = 0;
bool valid = 0;
if (sep) {
if (!config_wstr_to_int(sep + 1, &maxdigits)) {
valid = ((maxdigits == 0) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD));
Expand All @@ -1814,7 +1823,10 @@ config_init_int_max_str_digits(PyConfig *config)
#undef _STRINGIFY
#undef STRINGIFY
}
_Py_global_config_int_max_str_digits = maxdigits;
config->int_max_str_digits = maxdigits;
}
if (config->int_max_str_digits < 0) {
config->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS;
}
return _PyStatus_OK();
}
Expand Down Expand Up @@ -1882,7 +1894,7 @@ config_read_complex_options(PyConfig *config)
}
}

if (_Py_global_config_int_max_str_digits < 0) {
if (config->int_max_str_digits < 0) {
status = config_init_int_max_str_digits(config);
if (_PyStatus_EXCEPTION(status)) {
return status;
Expand Down
6 changes: 3 additions & 3 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1717,7 +1717,7 @@ sys_get_int_max_str_digits_impl(PyObject *module)
/*[clinic end generated code: output=0042f5e8ae0e8631 input=8dab13e2023e60d5]*/
{
PyInterpreterState *interp = _PyInterpreterState_GET();
return PyLong_FromSsize_t(interp->int_max_str_digits);
return PyLong_FromLong(interp->config.int_max_str_digits);
}

/*[clinic input]
Expand All @@ -1734,7 +1734,7 @@ sys_set_int_max_str_digits_impl(PyObject *module, int maxdigits)
{
PyThreadState *tstate = _PyThreadState_GET();
if ((!maxdigits) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD)) {
tstate->interp->int_max_str_digits = maxdigits;
tstate->interp->config.int_max_str_digits = maxdigits;
Py_RETURN_NONE;
} else {
PyErr_Format(
Expand Down Expand Up @@ -2810,7 +2810,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
SetFlag(preconfig->utf8_mode);
SetFlag(config->warn_default_encoding);
SetFlagObj(PyBool_FromLong(config->safe_path));
SetFlag(_Py_global_config_int_max_str_digits);
SetFlag(config->int_max_str_digits);
#undef SetFlagObj
#undef SetFlag
return 0;
Expand Down