]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135763: AC: Implement ``allow_negative`` for ``Py_ssize_t`` (#138150)
authorChristoph Walcher <christoph-wa@gmx.de>
Mon, 1 Sep 2025 21:55:22 +0000 (23:55 +0200)
committerGitHub <noreply@github.com>
Mon, 1 Sep 2025 21:55:22 +0000 (22:55 +0100)
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Include/internal/pycore_abstract.h
Lib/test/clinic.test.c
Lib/test/test_clinic.py
Modules/_testclinic.c
Modules/clinic/_testclinic.c.h
Python/modsupport.c
Tools/clinic/libclinic/converters.py

index 3cc0afac4bd5b45490d3edd8e92062bec837da8a..30809e097002ddf06a9e3100678395f757b25d8d 100644 (file)
@@ -51,6 +51,10 @@ extern int _PyObject_RealIsSubclass(PyObject *derived, PyObject *cls);
 // Export for '_bisect' shared extension.
 PyAPI_FUNC(int) _Py_convert_optional_to_ssize_t(PyObject *, void *);
 
+// Convert Python int to Py_ssize_t. Do nothing if the argument is None.
+// Raises ValueError if argument is negative.
+PyAPI_FUNC(int) _Py_convert_optional_to_non_negative_ssize_t(PyObject *, void *);
+
 // Same as PyNumber_Index() but can return an instance of a subclass of int.
 // Export for 'math' shared extension.
 PyAPI_FUNC(PyObject*) _PyNumber_Index(PyObject *o);
index f28b6a2456942b60f6298109820d9d85b7edd182..ed9058899c052385842368f0f7e7d23283904cfa 100644 (file)
@@ -1612,12 +1612,17 @@ test_Py_ssize_t_converter
     a: Py_ssize_t = 12
     b: Py_ssize_t(accept={int}) = 34
     c: Py_ssize_t(accept={int, NoneType}) = 56
+    d: Py_ssize_t(accept={int}, allow_negative=False) = 78
+    e: Py_ssize_t(accept={int, NoneType}, allow_negative=False) = 90
+    f: Py_ssize_t(accept={int}, allow_negative=True) = -12
+    g: Py_ssize_t(accept={int, NoneType}, allow_negative=True) = -34
     /
 
 [clinic start generated code]*/
 
 PyDoc_STRVAR(test_Py_ssize_t_converter__doc__,
-"test_Py_ssize_t_converter($module, a=12, b=34, c=56, /)\n"
+"test_Py_ssize_t_converter($module, a=12, b=34, c=56, d=78, e=90, f=-12,\n"
+"                          g=-34, /)\n"
 "--\n"
 "\n");
 
@@ -1626,7 +1631,8 @@ PyDoc_STRVAR(test_Py_ssize_t_converter__doc__,
 
 static PyObject *
 test_Py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
-                               Py_ssize_t c);
+                               Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
+                               Py_ssize_t f, Py_ssize_t g);
 
 static PyObject *
 test_Py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
@@ -1635,8 +1641,12 @@ test_Py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t na
     Py_ssize_t a = 12;
     Py_ssize_t b = 34;
     Py_ssize_t c = 56;
+    Py_ssize_t d = 78;
+    Py_ssize_t e = 90;
+    Py_ssize_t f = -12;
+    Py_ssize_t g = -34;
 
-    if (!_PyArg_CheckPositional("test_Py_ssize_t_converter", nargs, 0, 3)) {
+    if (!_PyArg_CheckPositional("test_Py_ssize_t_converter", nargs, 0, 7)) {
         goto exit;
     }
     if (nargs < 1) {
@@ -1675,8 +1685,55 @@ test_Py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t na
     if (!_Py_convert_optional_to_ssize_t(args[2], &c)) {
         goto exit;
     }
+    if (nargs < 4) {
+        goto skip_optional;
+    }
+    {
+        Py_ssize_t ival = -1;
+        PyObject *iobj = _PyNumber_Index(args[3]);
+        if (iobj != NULL) {
+            ival = PyLong_AsSsize_t(iobj);
+            Py_DECREF(iobj);
+        }
+        if (ival == -1 && PyErr_Occurred()) {
+            goto exit;
+        }
+        d = ival;
+        if (d < 0) {
+            PyErr_SetString(PyExc_ValueError,
+                            "d cannot be negative");
+            goto exit;
+        }
+    }
+    if (nargs < 5) {
+        goto skip_optional;
+    }
+    if (!_Py_convert_optional_to_non_negative_ssize_t(args[4], &e)) {
+        goto exit;
+    }
+    if (nargs < 6) {
+        goto skip_optional;
+    }
+    {
+        Py_ssize_t ival = -1;
+        PyObject *iobj = _PyNumber_Index(args[5]);
+        if (iobj != NULL) {
+            ival = PyLong_AsSsize_t(iobj);
+            Py_DECREF(iobj);
+        }
+        if (ival == -1 && PyErr_Occurred()) {
+            goto exit;
+        }
+        f = ival;
+    }
+    if (nargs < 7) {
+        goto skip_optional;
+    }
+    if (!_Py_convert_optional_to_ssize_t(args[6], &g)) {
+        goto exit;
+    }
 skip_optional:
-    return_value = test_Py_ssize_t_converter_impl(module, a, b, c);
+    return_value = test_Py_ssize_t_converter_impl(module, a, b, c, d, e, f, g);
 
 exit:
     return return_value;
@@ -1684,8 +1741,9 @@ exit:
 
 static PyObject *
 test_Py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
-                               Py_ssize_t c)
-/*[clinic end generated code: output=48214bc3d01f4dd7 input=3855f184bb3f299d]*/
+                               Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
+                               Py_ssize_t f, Py_ssize_t g)
+/*[clinic end generated code: output=4ae0a56a1447fba9 input=a25bac8ecf2890aa]*/
 
 
 /*[clinic input]
index f8d9b0af8f92ec26ef9c3454d94ca7fa34c7c359..2cc1aaea0ecbe774b383b2c988ec34c324d5d00c 100644 (file)
@@ -2610,6 +2610,19 @@ class ClinicParserTest(TestCase):
         """
         self.expect_failure(block, err, lineno=2)
 
+    def test_allow_negative_accepted_by_py_ssize_t_converter_only(self):
+        errmsg = re.escape("converter_init() got an unexpected keyword argument 'allow_negative'")
+        unsupported_converters = [converter_name for converter_name in converters.keys()
+                                  if converter_name != "Py_ssize_t"]
+        for converter in unsupported_converters:
+            with self.subTest(converter=converter):
+                block = f"""
+                    module m
+                    m.func
+                        a: {converter}(allow_negative=True)
+                """
+                with self.assertRaisesRegex((AssertionError, TypeError), errmsg):
+                    self.parse_function(block)
 
 class ClinicExternalTest(TestCase):
     maxDiff = None
@@ -3194,8 +3207,12 @@ class ClinicFunctionalTest(unittest.TestCase):
             ac_tester.py_ssize_t_converter(PY_SSIZE_T_MAX + 1)
         with self.assertRaises(TypeError):
             ac_tester.py_ssize_t_converter([])
-        self.assertEqual(ac_tester.py_ssize_t_converter(), (12, 34, 56))
-        self.assertEqual(ac_tester.py_ssize_t_converter(1, 2, None), (1, 2, 56))
+        with self.assertRaises(ValueError):
+            ac_tester.py_ssize_t_converter(12, 34, 56, -1)
+        with self.assertRaises(ValueError):
+            ac_tester.py_ssize_t_converter(12, 34, 56, 78, -1)
+        self.assertEqual(ac_tester.py_ssize_t_converter(), (12, 34, 56, 78, 90, -12, -34))
+        self.assertEqual(ac_tester.py_ssize_t_converter(1, 2, None, 3, None, 4, None), (1, 2, 56, 3, 90, 4, -34))
 
     def test_slice_index_converter(self):
         from _testcapi import PY_SSIZE_T_MIN, PY_SSIZE_T_MAX
index 3e903b6d87d89fcc4cef831d6260e532ab117eb2..64cefdc7f0b8e4ce7703191477ae7c9cc8752501 100644 (file)
@@ -443,16 +443,21 @@ py_ssize_t_converter
     a: Py_ssize_t = 12
     b: Py_ssize_t(accept={int}) = 34
     c: Py_ssize_t(accept={int, NoneType}) = 56
+    d: Py_ssize_t(accept={int}, allow_negative=False) = 78
+    e: Py_ssize_t(accept={int, NoneType}, allow_negative=False) = 90
+    f: Py_ssize_t(accept={int}, allow_negative=False) = -12
+    g: Py_ssize_t(accept={int, NoneType}, py_default="-34", allow_negative=False) = -34
     /
 
 [clinic start generated code]*/
 
 static PyObject *
 py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
-                          Py_ssize_t c)
-/*[clinic end generated code: output=ce252143e0ed0372 input=76d0f342e9317a1f]*/
+                          Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
+                          Py_ssize_t f, Py_ssize_t g)
+/*[clinic end generated code: output=ecf8e1a4a9abc95e input=7b7fa954780c1cb0]*/
 {
-    RETURN_PACKED_ARGS(3, PyLong_FromSsize_t, Py_ssize_t, a, b, c);
+    RETURN_PACKED_ARGS(7, PyLong_FromSsize_t, Py_ssize_t, a, b, c, d, e, f, g);
 }
 
 
index 68c92a86226bc98c61bd3bd40b1ba6af37b7c0d0..7e971f7ad7324c8c641d726a6c2638c365103467 100644 (file)
@@ -1196,7 +1196,8 @@ exit:
 }
 
 PyDoc_STRVAR(py_ssize_t_converter__doc__,
-"py_ssize_t_converter($module, a=12, b=34, c=56, /)\n"
+"py_ssize_t_converter($module, a=12, b=34, c=56, d=78, e=90, f=-12,\n"
+"                     g=-34, /)\n"
 "--\n"
 "\n");
 
@@ -1205,7 +1206,8 @@ PyDoc_STRVAR(py_ssize_t_converter__doc__,
 
 static PyObject *
 py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
-                          Py_ssize_t c);
+                          Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
+                          Py_ssize_t f, Py_ssize_t g);
 
 static PyObject *
 py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
@@ -1214,8 +1216,12 @@ py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
     Py_ssize_t a = 12;
     Py_ssize_t b = 34;
     Py_ssize_t c = 56;
+    Py_ssize_t d = 78;
+    Py_ssize_t e = 90;
+    Py_ssize_t f = -12;
+    Py_ssize_t g = -34;
 
-    if (!_PyArg_CheckPositional("py_ssize_t_converter", nargs, 0, 3)) {
+    if (!_PyArg_CheckPositional("py_ssize_t_converter", nargs, 0, 7)) {
         goto exit;
     }
     if (nargs < 1) {
@@ -1254,8 +1260,60 @@ py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
     if (!_Py_convert_optional_to_ssize_t(args[2], &c)) {
         goto exit;
     }
+    if (nargs < 4) {
+        goto skip_optional;
+    }
+    {
+        Py_ssize_t ival = -1;
+        PyObject *iobj = _PyNumber_Index(args[3]);
+        if (iobj != NULL) {
+            ival = PyLong_AsSsize_t(iobj);
+            Py_DECREF(iobj);
+        }
+        if (ival == -1 && PyErr_Occurred()) {
+            goto exit;
+        }
+        d = ival;
+        if (d < 0) {
+            PyErr_SetString(PyExc_ValueError,
+                            "d cannot be negative");
+            goto exit;
+        }
+    }
+    if (nargs < 5) {
+        goto skip_optional;
+    }
+    if (!_Py_convert_optional_to_non_negative_ssize_t(args[4], &e)) {
+        goto exit;
+    }
+    if (nargs < 6) {
+        goto skip_optional;
+    }
+    {
+        Py_ssize_t ival = -1;
+        PyObject *iobj = _PyNumber_Index(args[5]);
+        if (iobj != NULL) {
+            ival = PyLong_AsSsize_t(iobj);
+            Py_DECREF(iobj);
+        }
+        if (ival == -1 && PyErr_Occurred()) {
+            goto exit;
+        }
+        f = ival;
+        if (f < 0) {
+            PyErr_SetString(PyExc_ValueError,
+                            "f cannot be negative");
+            goto exit;
+        }
+    }
+    if (nargs < 7) {
+        goto skip_optional;
+    }
+    if (!_Py_convert_optional_to_non_negative_ssize_t(args[6], &g)) {
+        goto exit;
+    }
 skip_optional:
-    return_value = py_ssize_t_converter_impl(module, a, b, c);
+    return_value = py_ssize_t_converter_impl(module, a, b, c, d, e, f, g);
 
 exit:
     return return_value;
@@ -4542,4 +4600,4 @@ _testclinic_TestClass_posonly_poskw_varpos_array_no_fastcall(PyObject *type, PyO
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=6b04671afdafbecf input=a9049054013a1b77]*/
+/*[clinic end generated code: output=0764e6f8c9d94057 input=a9049054013a1b77]*/
index 437ad412027e28ebbb17ac25c91af71d5172c800..17b559e57fa1e794a4cd2af7f40ae75420773dc6 100644 (file)
@@ -33,6 +33,19 @@ _Py_convert_optional_to_ssize_t(PyObject *obj, void *result)
     return 1;
 }
 
+int
+_Py_convert_optional_to_non_negative_ssize_t(PyObject *obj, void *result)
+{
+    if (!_Py_convert_optional_to_ssize_t(obj, result)) {
+        return 0;
+    }
+    if (obj != Py_None && *((Py_ssize_t *)result) < 0) {
+        PyErr_SetString(PyExc_ValueError, "argument cannot be negative");
+        return 0;
+    }
+    return 1;
+}
+
 
 /* Helper for mkvalue() to scan the length of a format */
 
index 6e89e8de7cccf1c51c23885493bd7b9764760aae..d9f93b93d7587568cd82c612f77e417525d5375a 100644 (file)
@@ -420,21 +420,39 @@ class Py_ssize_t_converter(CConverter):
     type = 'Py_ssize_t'
     c_ignored_default = "0"
 
-    def converter_init(self, *, accept: TypeSet = {int}) -> None:
+    def converter_init(self, *, accept: TypeSet = {int},
+                       allow_negative: bool = True) -> None:
+        self.allow_negative = allow_negative
         if accept == {int}:
             self.format_unit = 'n'
             self.default_type = int
         elif accept == {int, NoneType}:
-            self.converter = '_Py_convert_optional_to_ssize_t'
+            if self.allow_negative:
+                self.converter = '_Py_convert_optional_to_ssize_t'
+            else:
+                self.converter = '_Py_convert_optional_to_non_negative_ssize_t'
         else:
             fail(f"Py_ssize_t_converter: illegal 'accept' argument {accept!r}")
 
     def use_converter(self) -> None:
-        if self.converter == '_Py_convert_optional_to_ssize_t':
-            self.add_include('pycore_abstract.h',
-                             '_Py_convert_optional_to_ssize_t()')
+        if self.converter in {
+            '_Py_convert_optional_to_ssize_t',
+            '_Py_convert_optional_to_non_negative_ssize_t',
+        }:
+            self.add_include('pycore_abstract.h', f'{self.converter}()')
 
     def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.allow_negative:
+            non_negative_check = ''
+        else:
+            non_negative_check = self.format_code("""
+                    if ({paramname} < 0) {{{{
+                        PyErr_SetString(PyExc_ValueError,
+                                        "{paramname} cannot be negative");
+                        goto exit;
+                    }}}}""",
+                argname=argname,
+            )
         if self.format_unit == 'n':
             if limited_capi:
                 PyNumber_Index = 'PyNumber_Index'
@@ -452,11 +470,13 @@ class Py_ssize_t_converter(CConverter):
                     if (ival == -1 && PyErr_Occurred()) {{{{
                         goto exit;
                     }}}}
-                    {paramname} = ival;
+                    {paramname} = ival;{non_negative_check}
                 }}}}
                 """,
                 argname=argname,
-                PyNumber_Index=PyNumber_Index)
+                PyNumber_Index=PyNumber_Index,
+                non_negative_check=non_negative_check,
+            )
         if not limited_capi:
             return super().parse_arg(argname, displayname, limited_capi=limited_capi)
         return self.format_code("""
@@ -465,7 +485,7 @@ class Py_ssize_t_converter(CConverter):
                     {paramname} = PyNumber_AsSsize_t({argname}, PyExc_OverflowError);
                     if ({paramname} == -1 && PyErr_Occurred()) {{{{
                         goto exit;
-                    }}}}
+                    }}}}{non_negative_check}
                 }}}}
                 else {{{{
                     {bad_argument}
@@ -475,6 +495,7 @@ class Py_ssize_t_converter(CConverter):
             """,
             argname=argname,
             bad_argument=self.bad_argument(displayname, 'integer or None', limited_capi=limited_capi),
+            non_negative_check=non_negative_check,
         )