Skip to content

Commit b0f89cb

Browse files
authored
gh-96512: Move int_max_str_digits setting to PyConfig (#96944)
It had to live as a global outside of PyConfig for stable ABI reasons in the pre-3.12 backports. This removes the `_Py_global_config_int_max_str_digits` and gets rid of the equivalent field in the internal `struct _is PyInterpreterState` as code can just use the existing nested config struct within that. Adds tests to verify unique settings and configs in subinterpreters.
1 parent cfbc7dd commit b0f89cb

File tree

13 files changed

+111
-24
lines changed

13 files changed

+111
-24
lines changed

Doc/c-api/init_config.rst

+18
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,24 @@ PyConfig
828828
829829
Default: ``0``.
830830
831+
.. c:member:: int int_max_str_digits
832+
833+
Configures the :ref:`integer string conversion length limitation
834+
<int_max_str_digits>`. An initial value of ``-1`` means the value will
835+
be taken from the command line or environment or otherwise default to
836+
4300 (:data:`sys.int_info.default_max_str_digits`). A value of ``0``
837+
disables the limitation. Values greater than zero but less than 640
838+
(:data:`sys.int_info.str_digits_check_threshold`) are unsupported and
839+
will produce an error.
840+
841+
Configured by the :option:`-X int_max_str_digits <-X>` command line
842+
flag or the :envvar:`PYTHONINTMAXSTRDIGITS` environment varable.
843+
844+
Default: ``-1`` in Python mode. 4300
845+
(:data:`sys.int_info.default_max_str_digits`) in isolated mode.
846+
847+
.. versionadded:: 3.12
848+
831849
.. c:member:: int isolated
832850
833851
If greater than ``0``, enable isolated mode:

Include/cpython/initconfig.h

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ typedef struct PyConfig {
178178
wchar_t *check_hash_pycs_mode;
179179
int use_frozen_modules;
180180
int safe_path;
181+
int int_max_str_digits;
181182

182183
/* --- Path configuration inputs ------------ */
183184
int pathconfig_warnings;

Include/internal/pycore_initconfig.h

-2
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,6 @@ extern void _Py_DumpPathConfig(PyThreadState *tstate);
170170

171171
PyAPI_FUNC(PyObject*) _Py_Get_Getpath_CodeObject(void);
172172

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

176174
/* --- Function used for testing ---------------------------------- */
177175

Include/internal/pycore_interp.h

-2
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ struct _is {
175175
struct types_state types;
176176
struct callable_cache callable_cache;
177177

178-
int int_max_str_digits;
179-
180178
/* The following fields are here to avoid allocation during init.
181179
The data is exposed through PyInterpreterState pointer fields.
182180
These fields should not be accessed directly outside of init.

Lib/test/test_capi.py

+33
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,39 @@ async def foo(arg): return await arg # Py 3.5
999999
self.assertEqual(ret, 0)
10001000
self.assertEqual(pickle.load(f), {'a': '123x', 'b': '123'})
10011001

1002+
def test_py_config_isoloated_per_interpreter(self):
1003+
# A config change in one interpreter must not leak to out to others.
1004+
#
1005+
# This test could verify ANY config value, it just happens to have been
1006+
# written around the time of int_max_str_digits. Refactoring is okay.
1007+
code = """if 1:
1008+
import sys, _testinternalcapi
1009+
1010+
# Any config value would do, this happens to be the one being
1011+
# double checked at the time this test was written.
1012+
config = _testinternalcapi.get_config()
1013+
config['int_max_str_digits'] = 55555
1014+
_testinternalcapi.set_config(config)
1015+
sub_value = _testinternalcapi.get_config()['int_max_str_digits']
1016+
assert sub_value == 55555, sub_value
1017+
"""
1018+
before_config = _testinternalcapi.get_config()
1019+
assert before_config['int_max_str_digits'] != 55555
1020+
self.assertEqual(support.run_in_subinterp(code), 0,
1021+
'subinterp code failure, check stderr.')
1022+
after_config = _testinternalcapi.get_config()
1023+
self.assertIsNot(
1024+
before_config, after_config,
1025+
"Expected get_config() to return a new dict on each call")
1026+
self.assertEqual(before_config, after_config,
1027+
"CAUTION: Tests executed after this may be "
1028+
"running under an altered config.")
1029+
# try:...finally: calling set_config(before_config) not done
1030+
# as that results in sys.argv, sys.path, and sys.warnoptions
1031+
# "being modified by test_capi" per test.regrtest. So if this
1032+
# test fails, assume that the environment in this process may
1033+
# be altered and suspect.
1034+
10021035
def test_mutate_exception(self):
10031036
"""
10041037
Exceptions saved in global module state get shared between

Lib/test/test_cmd_line.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,8 @@ def res2int(res):
882882
return tuple(int(i) for i in out.split())
883883

884884
res = assert_python_ok('-c', code)
885-
self.assertEqual(res2int(res), (-1, sys.get_int_max_str_digits()))
885+
current_max = sys.get_int_max_str_digits()
886+
self.assertEqual(res2int(res), (current_max, current_max))
886887
res = assert_python_ok('-X', 'int_max_str_digits=0', '-c', code)
887888
self.assertEqual(res2int(res), (0, 0))
888889
res = assert_python_ok('-X', 'int_max_str_digits=4000', '-c', code)

Lib/test/test_embed.py

+4
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
434434
'install_signal_handlers': 1,
435435
'use_hash_seed': 0,
436436
'hash_seed': 0,
437+
'int_max_str_digits': sys.int_info.default_max_str_digits,
437438
'faulthandler': 0,
438439
'tracemalloc': 0,
439440
'perf_profiling': 0,
@@ -876,6 +877,7 @@ def test_init_from_config(self):
876877
'platlibdir': 'my_platlibdir',
877878
'module_search_paths': self.IGNORE_CONFIG,
878879
'safe_path': 1,
880+
'int_max_str_digits': 31337,
879881

880882
'check_hash_pycs_mode': 'always',
881883
'pathconfig_warnings': 0,
@@ -912,6 +914,7 @@ def test_init_compat_env(self):
912914
'platlibdir': 'env_platlibdir',
913915
'module_search_paths': self.IGNORE_CONFIG,
914916
'safe_path': 1,
917+
'int_max_str_digits': 4567,
915918
}
916919
self.check_all_configs("test_init_compat_env", config, preconfig,
917920
api=API_COMPAT)
@@ -944,6 +947,7 @@ def test_init_python_env(self):
944947
'platlibdir': 'env_platlibdir',
945948
'module_search_paths': self.IGNORE_CONFIG,
946949
'safe_path': 1,
950+
'int_max_str_digits': 4567,
947951
}
948952
self.check_all_configs("test_init_python_env", config, preconfig,
949953
api=API_PYTHON)

Lib/test/test_int.py

+20
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,26 @@ def test_int_from_other_bases(self):
770770
with self.subTest(base=base):
771771
self._other_base_helper(base)
772772

773+
def test_int_max_str_digits_is_per_interpreter(self):
774+
# Changing the limit in one interpreter does not change others.
775+
code = """if 1:
776+
# Subinterpreters maintain and enforce their own limit
777+
import sys
778+
sys.set_int_max_str_digits(2323)
779+
try:
780+
int('3'*3333)
781+
except ValueError:
782+
pass
783+
else:
784+
raise AssertionError('Expected a int max str digits ValueError.')
785+
"""
786+
with support.adjust_int_max_str_digits(4000):
787+
before_value = sys.get_int_max_str_digits()
788+
self.assertEqual(support.run_in_subinterp(code), 0,
789+
'subinterp code failure, check stderr.')
790+
after_value = sys.get_int_max_str_digits()
791+
self.assertEqual(before_value, after_value)
792+
773793

774794
class IntSubclassStrDigitLimitsTests(IntStrDigitLimitsTests):
775795
int_class = IntSubclass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Configuration for the :ref:`integer string conversion length limitation
2+
<int_max_str_digits>` now lives in the PyConfig C API struct.

Objects/longobject.c

+3-7
Original file line numberDiff line numberDiff line change
@@ -1767,7 +1767,7 @@ long_to_decimal_string_internal(PyObject *aa,
17671767
if (size_a >= 10 * _PY_LONG_MAX_STR_DIGITS_THRESHOLD
17681768
/ (3 * PyLong_SHIFT) + 2) {
17691769
PyInterpreterState *interp = _PyInterpreterState_GET();
1770-
int max_str_digits = interp->int_max_str_digits;
1770+
int max_str_digits = interp->config.int_max_str_digits;
17711771
if ((max_str_digits > 0) &&
17721772
(max_str_digits / (3 * PyLong_SHIFT) <= (size_a - 11) / 10)) {
17731773
PyErr_Format(PyExc_ValueError, _MAX_STR_DIGITS_ERROR_FMT_TO_STR,
@@ -1837,7 +1837,7 @@ long_to_decimal_string_internal(PyObject *aa,
18371837
}
18381838
if (strlen > _PY_LONG_MAX_STR_DIGITS_THRESHOLD) {
18391839
PyInterpreterState *interp = _PyInterpreterState_GET();
1840-
int max_str_digits = interp->int_max_str_digits;
1840+
int max_str_digits = interp->config.int_max_str_digits;
18411841
Py_ssize_t strlen_nosign = strlen - negative;
18421842
if ((max_str_digits > 0) && (strlen_nosign > max_str_digits)) {
18431843
Py_DECREF(scratch);
@@ -2578,7 +2578,7 @@ long_from_string_base(const char **str, int base, PyLongObject **res)
25782578
* quadratic algorithm. */
25792579
if (digits > _PY_LONG_MAX_STR_DIGITS_THRESHOLD) {
25802580
PyInterpreterState *interp = _PyInterpreterState_GET();
2581-
int max_str_digits = interp->int_max_str_digits;
2581+
int max_str_digits = interp->config.int_max_str_digits;
25822582
if ((max_str_digits > 0) && (digits > max_str_digits)) {
25832583
PyErr_Format(PyExc_ValueError, _MAX_STR_DIGITS_ERROR_FMT_TO_INT,
25842584
max_str_digits, digits);
@@ -6235,10 +6235,6 @@ _PyLong_InitTypes(PyInterpreterState *interp)
62356235
return _PyStatus_ERR("can't init int info type");
62366236
}
62376237
}
6238-
interp->int_max_str_digits = _Py_global_config_int_max_str_digits;
6239-
if (interp->int_max_str_digits == -1) {
6240-
interp->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS;
6241-
}
62426238

62436239
return _PyStatus_OK();
62446240
}

Programs/_testembed.c

+4
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,9 @@ static int test_init_from_config(void)
683683

684684
config._isolated_interpreter = 1;
685685

686+
putenv("PYTHONINTMAXSTRDIGITS=6666");
687+
config.int_max_str_digits = 31337;
688+
686689
init_from_config_clear(&config);
687690

688691
dump_config();
@@ -748,6 +751,7 @@ static void set_most_env_vars(void)
748751
putenv("PYTHONIOENCODING=iso8859-1:replace");
749752
putenv("PYTHONPLATLIBDIR=env_platlibdir");
750753
putenv("PYTHONSAFEPATH=1");
754+
putenv("PYTHONINTMAXSTRDIGITS=4567");
751755
}
752756

753757

Python/initconfig.c

+21-9
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,7 @@ config_check_consistency(const PyConfig *config)
695695
assert(config->pathconfig_warnings >= 0);
696696
assert(config->_is_python_build >= 0);
697697
assert(config->safe_path >= 0);
698+
assert(config->int_max_str_digits >= 0);
698699
// config->use_frozen_modules is initialized later
699700
// by _PyConfig_InitImportConfig().
700701
return 1;
@@ -789,14 +790,11 @@ _PyConfig_InitCompatConfig(PyConfig *config)
789790
config->use_frozen_modules = 1;
790791
#endif
791792
config->safe_path = 0;
793+
config->int_max_str_digits = -1;
792794
config->_is_python_build = 0;
793795
config->code_debug_ranges = 1;
794796
}
795797

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

801799
static void
802800
config_init_defaults(PyConfig *config)
@@ -849,6 +847,7 @@ PyConfig_InitIsolatedConfig(PyConfig *config)
849847
config->faulthandler = 0;
850848
config->tracemalloc = 0;
851849
config->perf_profiling = 0;
850+
config->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS;
852851
config->safe_path = 1;
853852
config->pathconfig_warnings = 0;
854853
#ifdef MS_WINDOWS
@@ -1021,6 +1020,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
10211020
COPY_ATTR(safe_path);
10221021
COPY_WSTRLIST(orig_argv);
10231022
COPY_ATTR(_is_python_build);
1023+
COPY_ATTR(int_max_str_digits);
10241024

10251025
#undef COPY_ATTR
10261026
#undef COPY_WSTR_ATTR
@@ -1128,6 +1128,7 @@ _PyConfig_AsDict(const PyConfig *config)
11281128
SET_ITEM_INT(use_frozen_modules);
11291129
SET_ITEM_INT(safe_path);
11301130
SET_ITEM_INT(_is_python_build);
1131+
SET_ITEM_INT(int_max_str_digits);
11311132

11321133
return dict;
11331134

@@ -1317,6 +1318,12 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
13171318
} \
13181319
CHECK_VALUE(#KEY, config->KEY >= 0); \
13191320
} while (0)
1321+
#define GET_INT(KEY) \
1322+
do { \
1323+
if (config_dict_get_int(dict, #KEY, &config->KEY) < 0) { \
1324+
return -1; \
1325+
} \
1326+
} while (0)
13201327
#define GET_WSTR(KEY) \
13211328
do { \
13221329
if (config_dict_get_wstr(dict, #KEY, config, &config->KEY) < 0) { \
@@ -1415,9 +1422,11 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
14151422
GET_UINT(use_frozen_modules);
14161423
GET_UINT(safe_path);
14171424
GET_UINT(_is_python_build);
1425+
GET_INT(int_max_str_digits);
14181426

14191427
#undef CHECK_VALUE
14201428
#undef GET_UINT
1429+
#undef GET_INT
14211430
#undef GET_WSTR
14221431
#undef GET_WSTR_OPT
14231432
return 0;
@@ -1782,7 +1791,7 @@ config_init_int_max_str_digits(PyConfig *config)
17821791

17831792
const char *env = config_get_env(config, "PYTHONINTMAXSTRDIGITS");
17841793
if (env) {
1785-
int valid = 0;
1794+
bool valid = 0;
17861795
if (!_Py_str_to_int(env, &maxdigits)) {
17871796
valid = ((maxdigits == 0) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD));
17881797
}
@@ -1794,13 +1803,13 @@ config_init_int_max_str_digits(PyConfig *config)
17941803
STRINGIFY(_PY_LONG_MAX_STR_DIGITS_THRESHOLD)
17951804
" or 0 for unlimited.");
17961805
}
1797-
_Py_global_config_int_max_str_digits = maxdigits;
1806+
config->int_max_str_digits = maxdigits;
17981807
}
17991808

18001809
const wchar_t *xoption = config_get_xoption(config, L"int_max_str_digits");
18011810
if (xoption) {
18021811
const wchar_t *sep = wcschr(xoption, L'=');
1803-
int valid = 0;
1812+
bool valid = 0;
18041813
if (sep) {
18051814
if (!config_wstr_to_int(sep + 1, &maxdigits)) {
18061815
valid = ((maxdigits == 0) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD));
@@ -1814,7 +1823,10 @@ config_init_int_max_str_digits(PyConfig *config)
18141823
#undef _STRINGIFY
18151824
#undef STRINGIFY
18161825
}
1817-
_Py_global_config_int_max_str_digits = maxdigits;
1826+
config->int_max_str_digits = maxdigits;
1827+
}
1828+
if (config->int_max_str_digits < 0) {
1829+
config->int_max_str_digits = _PY_LONG_DEFAULT_MAX_STR_DIGITS;
18181830
}
18191831
return _PyStatus_OK();
18201832
}
@@ -1882,7 +1894,7 @@ config_read_complex_options(PyConfig *config)
18821894
}
18831895
}
18841896

1885-
if (_Py_global_config_int_max_str_digits < 0) {
1897+
if (config->int_max_str_digits < 0) {
18861898
status = config_init_int_max_str_digits(config);
18871899
if (_PyStatus_EXCEPTION(status)) {
18881900
return status;

Python/sysmodule.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -1717,7 +1717,7 @@ sys_get_int_max_str_digits_impl(PyObject *module)
17171717
/*[clinic end generated code: output=0042f5e8ae0e8631 input=8dab13e2023e60d5]*/
17181718
{
17191719
PyInterpreterState *interp = _PyInterpreterState_GET();
1720-
return PyLong_FromSsize_t(interp->int_max_str_digits);
1720+
return PyLong_FromLong(interp->config.int_max_str_digits);
17211721
}
17221722

17231723
/*[clinic input]
@@ -1734,7 +1734,7 @@ sys_set_int_max_str_digits_impl(PyObject *module, int maxdigits)
17341734
{
17351735
PyThreadState *tstate = _PyThreadState_GET();
17361736
if ((!maxdigits) || (maxdigits >= _PY_LONG_MAX_STR_DIGITS_THRESHOLD)) {
1737-
tstate->interp->int_max_str_digits = maxdigits;
1737+
tstate->interp->config.int_max_str_digits = maxdigits;
17381738
Py_RETURN_NONE;
17391739
} else {
17401740
PyErr_Format(
@@ -2810,7 +2810,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
28102810
SetFlag(preconfig->utf8_mode);
28112811
SetFlag(config->warn_default_encoding);
28122812
SetFlagObj(PyBool_FromLong(config->safe_path));
2813-
SetFlag(_Py_global_config_int_max_str_digits);
2813+
SetFlag(config->int_max_str_digits);
28142814
#undef SetFlagObj
28152815
#undef SetFlag
28162816
return 0;

0 commit comments

Comments
 (0)