[Python-checkins] gh-96512: Move int_max_str_digits setting to PyConfig (#96944)

gpshead webhook-mailer at python.org
Mon Oct 3 16:55:54 EDT 2022


https://github.com/python/cpython/commit/b0f89cb4311b696f875e58f14258ce315be09bce
commit: b0f89cb4311b696f875e58f14258ce315be09bce
branch: main
author: Gregory P. Smith <greg at krypto.org>
committer: gpshead <greg at krypto.org>
date: 2022-10-03T13:55:45-07:00
summary:

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.

files:
A Misc/NEWS.d/next/C API/2022-09-20-01-04-57.gh-issue-96512.msZTjF.rst
M Doc/c-api/init_config.rst
M Include/cpython/initconfig.h
M Include/internal/pycore_initconfig.h
M Include/internal/pycore_interp.h
M Lib/test/test_capi.py
M Lib/test/test_cmd_line.py
M Lib/test/test_embed.py
M Lib/test/test_int.py
M Objects/longobject.c
M Programs/_testembed.c
M Python/initconfig.c
M Python/sysmodule.c

diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst
index c4a342ee811c..ea76315fc09b 100644
--- a/Doc/c-api/init_config.rst
+++ b/Doc/c-api/init_config.rst
@@ -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.
+
+      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:
diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h
index c6057a4c3ed9..c22c8d52b4f2 100644
--- a/Include/cpython/initconfig.h
+++ b/Include/cpython/initconfig.h
@@ -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;
diff --git a/Include/internal/pycore_initconfig.h b/Include/internal/pycore_initconfig.h
index 6e491261d55c..69f88d7d1d46 100644
--- a/Include/internal/pycore_initconfig.h
+++ b/Include/internal/pycore_initconfig.h
@@ -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 ---------------------------------- */
 
diff --git a/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h
index e7f914ec2fe5..b21708a388b3 100644
--- a/Include/internal/pycore_interp.h
+++ b/Include/internal/pycore_interp.h
@@ -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.
diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py
index 94f080978b03..2c6fe34d3b78 100644
--- a/Lib/test/test_capi.py
+++ b/Lib/test/test_capi.py
@@ -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
+        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
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index 3de8c3d4b11f..942980030635 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -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)
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index 6b5d48547b8a..c5aeb9459848 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -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,
@@ -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,
@@ -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)
@@ -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)
diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py
index c972b8afb48d..625c388cd947 100644
--- a/Lib/test/test_int.py
+++ b/Lib/test/test_int.py
@@ -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
diff --git a/Misc/NEWS.d/next/C API/2022-09-20-01-04-57.gh-issue-96512.msZTjF.rst b/Misc/NEWS.d/next/C API/2022-09-20-01-04-57.gh-issue-96512.msZTjF.rst
new file mode 100644
index 000000000000..787bee3ee23b
--- /dev/null
+++ b/Misc/NEWS.d/next/C API/2022-09-20-01-04-57.gh-issue-96512.msZTjF.rst	
@@ -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.
diff --git a/Objects/longobject.c b/Objects/longobject.c
index 77a8782d8a67..d9f3d393b998 100644
--- a/Objects/longobject.c
+++ b/Objects/longobject.c
@@ -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,
@@ -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);
@@ -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);
@@ -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();
 }
diff --git a/Programs/_testembed.c b/Programs/_testembed.c
index e5b138ce84bb..d635c5a4abe3 100644
--- a/Programs/_testembed.c
+++ b/Programs/_testembed.c
@@ -683,6 +683,9 @@ static int test_init_from_config(void)
 
     config._isolated_interpreter = 1;
 
+    putenv("PYTHONINTMAXSTRDIGITS=6666");
+    config.int_max_str_digits = 31337;
+
     init_from_config_clear(&config);
 
     dump_config();
@@ -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");
 }
 
 
diff --git a/Python/initconfig.c b/Python/initconfig.c
index bfbb7dbacf90..bbc2ebb09fd0 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -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;
@@ -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)
@@ -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
@@ -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
@@ -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;
 
@@ -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) { \
@@ -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;
@@ -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));
         }
@@ -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));
@@ -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();
 }
@@ -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;
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index 653b5a55e885..584a8be7094b 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -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]
@@ -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(
@@ -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;



More information about the Python-checkins mailing list