diff --git a/Doc/c-api/arg.rst b/Doc/c-api/arg.rst index 3429a4eb652709..6f1e814b864aa2 100644 --- a/Doc/c-api/arg.rst +++ b/Doc/c-api/arg.rst @@ -285,6 +285,9 @@ the minimal value for the corresponding signed integer type of the same size. ``n`` (:class:`int`) [:c:type:`Py_ssize_t`] Convert a Python integer to a C :c:type:`Py_ssize_t`. +``N`` (:class:`int`) [:c:type:`Py_ssize_t`] + Convert a non-negative Python integer to a C :c:type:`Py_ssize_t`. + ``c`` (:class:`bytes` or :class:`bytearray` of length 1) [char] Convert a Python byte, represented as a :class:`bytes` or :class:`bytearray` object of length 1, to a C :c:expr:`char`. diff --git a/Lib/test/test_capi/test_getargs.py b/Lib/test/test_capi/test_getargs.py index 0b2473bac2be11..0af2aaed89c27d 100644 --- a/Lib/test/test_capi/test_getargs.py +++ b/Lib/test/test_capi/test_getargs.py @@ -290,6 +290,32 @@ def test_I(self): with self.assertWarns(DeprecationWarning): self.assertEqual(UINT_MAX & -VERY_LARGE, getargs_I(-VERY_LARGE)) + def test_N(self): + from _testcapi import getargs_N + # n returns 'Py_ssize_t', and does range checking + # (0 ... PY_SSIZE_T_MAX) + self.assertRaises(TypeError, getargs_N, 3.14) + self.assertEqual(99, getargs_N(Index())) + self.assertEqual(0, getargs_N(IndexIntSubclass())) + self.assertRaises(TypeError, getargs_N, BadIndex()) + with self.assertWarns(DeprecationWarning): + self.assertEqual(1, getargs_N(BadIndex2())) + self.assertEqual(0, getargs_N(BadIndex3())) + self.assertRaises(TypeError, getargs_N, Int()) + self.assertEqual(0, getargs_N(IntSubclass())) + self.assertRaises(TypeError, getargs_N, BadInt()) + self.assertRaises(TypeError, getargs_N, BadInt2()) + self.assertEqual(0, getargs_N(BadInt3())) + + self.assertRaises(OverflowError, getargs_N, PY_SSIZE_T_MIN-1) + self.assertRaises(OverflowError, getargs_N, PY_SSIZE_T_MIN) + self.assertRaises(OverflowError, getargs_N, -1) + self.assertEqual(PY_SSIZE_T_MAX, getargs_N(PY_SSIZE_T_MAX)) + self.assertRaises(OverflowError, getargs_N, PY_SSIZE_T_MAX+1) + + self.assertEqual(42, getargs_N(42)) + self.assertRaises(OverflowError, getargs_N, VERY_LARGE) + def test_k(self): from _testcapi import getargs_k # k returns 'unsigned long', no range checking diff --git a/Misc/NEWS.d/next/C_API/2025-09-01-22-45-45.gh-issue-138361.okpmAJ.rst b/Misc/NEWS.d/next/C_API/2025-09-01-22-45-45.gh-issue-138361.okpmAJ.rst new file mode 100644 index 00000000000000..a872fe3b36c542 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-09-01-22-45-45.gh-issue-138361.okpmAJ.rst @@ -0,0 +1,2 @@ +Added ``N`` as new ``PyArg_Parse`` format unit for non-negative ``Py_ssize_t`` +values. diff --git a/Modules/_testcapi/getargs.c b/Modules/_testcapi/getargs.c index ee04c760d27213..320c82a8d47d14 100644 --- a/Modules/_testcapi/getargs.c +++ b/Modules/_testcapi/getargs.c @@ -437,6 +437,16 @@ getargs_n(PyObject *self, PyObject *args) return PyLong_FromSsize_t(value); } +static PyObject * +getargs_N(PyObject *self, PyObject *args) +{ + Py_ssize_t value; + if (!PyArg_ParseTuple(args, "N", &value)) { + return NULL; + } + return PyLong_FromSsize_t(value); +} + static PyObject * getargs_p(PyObject *self, PyObject *args) { @@ -793,6 +803,7 @@ static PyMethodDef test_methods[] = { {"getargs_keywords", _PyCFunction_CAST(getargs_keywords), METH_VARARGS|METH_KEYWORDS}, {"getargs_l", getargs_l, METH_VARARGS}, {"getargs_n", getargs_n, METH_VARARGS}, + {"getargs_N", getargs_N, METH_VARARGS}, {"getargs_p", getargs_p, METH_VARARGS}, {"getargs_positional_only_and_keywords", _PyCFunction_CAST(getargs_positional_only_and_keywords), METH_VARARGS|METH_KEYWORDS}, {"getargs_s", getargs_s, METH_VARARGS}, diff --git a/Python/getargs.c b/Python/getargs.c index c119ca5c35398b..7f1363775950c2 100644 --- a/Python/getargs.c +++ b/Python/getargs.c @@ -814,6 +814,27 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, *p = ival; break; } + + case 'N': { /* Py_ssize_t - non negative */ + PyObject *iobj; + Py_ssize_t *p = va_arg(*p_va, Py_ssize_t *); + Py_ssize_t ival = -1; + iobj = _PyNumber_Index(arg); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) + RETURN_ERR_OCCURRED; + else if (ival < 0) { + PyErr_SetString(PyExc_OverflowError, + "integer is less than minimum"); + RETURN_ERR_OCCURRED; + } + *p = ival; + break; + } + case 'l': {/* long int */ long *p = va_arg(*p_va, long *); long ival = PyLong_AsLong(arg); @@ -2618,6 +2639,7 @@ skipitem(const char **p_format, va_list *p_va, int flags) case 'L': /* long long */ case 'K': /* long long sized bitfield */ case 'n': /* Py_ssize_t */ + case 'N': /* Py_ssize_t - non negative */ case 'f': /* float */ case 'd': /* double */ case 'D': /* complex double */