Skip to content

Commit 47bc10e

Browse files
wiomocAA-Turner
andauthored
gh-135763: AC: Implement allow_negative for Py_ssize_t (#138150)
Co-authored-by: Adam Turner <[email protected]>
1 parent 7c92497 commit 47bc10e

File tree

7 files changed

+200
-24
lines changed

7 files changed

+200
-24
lines changed

Include/internal/pycore_abstract.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ extern int _PyObject_RealIsSubclass(PyObject *derived, PyObject *cls);
5151
// Export for '_bisect' shared extension.
5252
PyAPI_FUNC(int) _Py_convert_optional_to_ssize_t(PyObject *, void *);
5353

54+
// Convert Python int to Py_ssize_t. Do nothing if the argument is None.
55+
// Raises ValueError if argument is negative.
56+
PyAPI_FUNC(int) _Py_convert_optional_to_non_negative_ssize_t(PyObject *, void *);
57+
5458
// Same as PyNumber_Index() but can return an instance of a subclass of int.
5559
// Export for 'math' shared extension.
5660
PyAPI_FUNC(PyObject*) _PyNumber_Index(PyObject *o);

Lib/test/clinic.test.c

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,12 +1612,17 @@ test_Py_ssize_t_converter
16121612
a: Py_ssize_t = 12
16131613
b: Py_ssize_t(accept={int}) = 34
16141614
c: Py_ssize_t(accept={int, NoneType}) = 56
1615+
d: Py_ssize_t(accept={int}, allow_negative=False) = 78
1616+
e: Py_ssize_t(accept={int, NoneType}, allow_negative=False) = 90
1617+
f: Py_ssize_t(accept={int}, allow_negative=True) = -12
1618+
g: Py_ssize_t(accept={int, NoneType}, allow_negative=True) = -34
16151619
/
16161620
16171621
[clinic start generated code]*/
16181622

16191623
PyDoc_STRVAR(test_Py_ssize_t_converter__doc__,
1620-
"test_Py_ssize_t_converter($module, a=12, b=34, c=56, /)\n"
1624+
"test_Py_ssize_t_converter($module, a=12, b=34, c=56, d=78, e=90, f=-12,\n"
1625+
" g=-34, /)\n"
16211626
"--\n"
16221627
"\n");
16231628

@@ -1626,7 +1631,8 @@ PyDoc_STRVAR(test_Py_ssize_t_converter__doc__,
16261631

16271632
static PyObject *
16281633
test_Py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
1629-
Py_ssize_t c);
1634+
Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
1635+
Py_ssize_t f, Py_ssize_t g);
16301636

16311637
static PyObject *
16321638
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
16351641
Py_ssize_t a = 12;
16361642
Py_ssize_t b = 34;
16371643
Py_ssize_t c = 56;
1644+
Py_ssize_t d = 78;
1645+
Py_ssize_t e = 90;
1646+
Py_ssize_t f = -12;
1647+
Py_ssize_t g = -34;
16381648

1639-
if (!_PyArg_CheckPositional("test_Py_ssize_t_converter", nargs, 0, 3)) {
1649+
if (!_PyArg_CheckPositional("test_Py_ssize_t_converter", nargs, 0, 7)) {
16401650
goto exit;
16411651
}
16421652
if (nargs < 1) {
@@ -1675,17 +1685,65 @@ test_Py_ssize_t_converter(PyObject *module, PyObject *const *args, Py_ssize_t na
16751685
if (!_Py_convert_optional_to_ssize_t(args[2], &c)) {
16761686
goto exit;
16771687
}
1688+
if (nargs < 4) {
1689+
goto skip_optional;
1690+
}
1691+
{
1692+
Py_ssize_t ival = -1;
1693+
PyObject *iobj = _PyNumber_Index(args[3]);
1694+
if (iobj != NULL) {
1695+
ival = PyLong_AsSsize_t(iobj);
1696+
Py_DECREF(iobj);
1697+
}
1698+
if (ival == -1 && PyErr_Occurred()) {
1699+
goto exit;
1700+
}
1701+
d = ival;
1702+
if (d < 0) {
1703+
PyErr_SetString(PyExc_ValueError,
1704+
"d cannot be negative");
1705+
goto exit;
1706+
}
1707+
}
1708+
if (nargs < 5) {
1709+
goto skip_optional;
1710+
}
1711+
if (!_Py_convert_optional_to_non_negative_ssize_t(args[4], &e)) {
1712+
goto exit;
1713+
}
1714+
if (nargs < 6) {
1715+
goto skip_optional;
1716+
}
1717+
{
1718+
Py_ssize_t ival = -1;
1719+
PyObject *iobj = _PyNumber_Index(args[5]);
1720+
if (iobj != NULL) {
1721+
ival = PyLong_AsSsize_t(iobj);
1722+
Py_DECREF(iobj);
1723+
}
1724+
if (ival == -1 && PyErr_Occurred()) {
1725+
goto exit;
1726+
}
1727+
f = ival;
1728+
}
1729+
if (nargs < 7) {
1730+
goto skip_optional;
1731+
}
1732+
if (!_Py_convert_optional_to_ssize_t(args[6], &g)) {
1733+
goto exit;
1734+
}
16781735
skip_optional:
1679-
return_value = test_Py_ssize_t_converter_impl(module, a, b, c);
1736+
return_value = test_Py_ssize_t_converter_impl(module, a, b, c, d, e, f, g);
16801737

16811738
exit:
16821739
return return_value;
16831740
}
16841741

16851742
static PyObject *
16861743
test_Py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
1687-
Py_ssize_t c)
1688-
/*[clinic end generated code: output=48214bc3d01f4dd7 input=3855f184bb3f299d]*/
1744+
Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
1745+
Py_ssize_t f, Py_ssize_t g)
1746+
/*[clinic end generated code: output=4ae0a56a1447fba9 input=a25bac8ecf2890aa]*/
16891747

16901748

16911749
/*[clinic input]

Lib/test/test_clinic.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2610,6 +2610,19 @@ def test_disallow_defining_class_at_module_level(self):
26102610
"""
26112611
self.expect_failure(block, err, lineno=2)
26122612

2613+
def test_allow_negative_accepted_by_py_ssize_t_converter_only(self):
2614+
errmsg = re.escape("converter_init() got an unexpected keyword argument 'allow_negative'")
2615+
unsupported_converters = [converter_name for converter_name in converters.keys()
2616+
if converter_name != "Py_ssize_t"]
2617+
for converter in unsupported_converters:
2618+
with self.subTest(converter=converter):
2619+
block = f"""
2620+
module m
2621+
m.func
2622+
a: {converter}(allow_negative=True)
2623+
"""
2624+
with self.assertRaisesRegex((AssertionError, TypeError), errmsg):
2625+
self.parse_function(block)
26132626

26142627
class ClinicExternalTest(TestCase):
26152628
maxDiff = None
@@ -3194,8 +3207,12 @@ def test_py_ssize_t_converter(self):
31943207
ac_tester.py_ssize_t_converter(PY_SSIZE_T_MAX + 1)
31953208
with self.assertRaises(TypeError):
31963209
ac_tester.py_ssize_t_converter([])
3197-
self.assertEqual(ac_tester.py_ssize_t_converter(), (12, 34, 56))
3198-
self.assertEqual(ac_tester.py_ssize_t_converter(1, 2, None), (1, 2, 56))
3210+
with self.assertRaises(ValueError):
3211+
ac_tester.py_ssize_t_converter(12, 34, 56, -1)
3212+
with self.assertRaises(ValueError):
3213+
ac_tester.py_ssize_t_converter(12, 34, 56, 78, -1)
3214+
self.assertEqual(ac_tester.py_ssize_t_converter(), (12, 34, 56, 78, 90, -12, -34))
3215+
self.assertEqual(ac_tester.py_ssize_t_converter(1, 2, None, 3, None, 4, None), (1, 2, 56, 3, 90, 4, -34))
31993216

32003217
def test_slice_index_converter(self):
32013218
from _testcapi import PY_SSIZE_T_MIN, PY_SSIZE_T_MAX

Modules/_testclinic.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,16 +443,21 @@ py_ssize_t_converter
443443
a: Py_ssize_t = 12
444444
b: Py_ssize_t(accept={int}) = 34
445445
c: Py_ssize_t(accept={int, NoneType}) = 56
446+
d: Py_ssize_t(accept={int}, allow_negative=False) = 78
447+
e: Py_ssize_t(accept={int, NoneType}, allow_negative=False) = 90
448+
f: Py_ssize_t(accept={int}, allow_negative=False) = -12
449+
g: Py_ssize_t(accept={int, NoneType}, py_default="-34", allow_negative=False) = -34
446450
/
447451
448452
[clinic start generated code]*/
449453

450454
static PyObject *
451455
py_ssize_t_converter_impl(PyObject *module, Py_ssize_t a, Py_ssize_t b,
452-
Py_ssize_t c)
453-
/*[clinic end generated code: output=ce252143e0ed0372 input=76d0f342e9317a1f]*/
456+
Py_ssize_t c, Py_ssize_t d, Py_ssize_t e,
457+
Py_ssize_t f, Py_ssize_t g)
458+
/*[clinic end generated code: output=ecf8e1a4a9abc95e input=7b7fa954780c1cb0]*/
454459
{
455-
RETURN_PACKED_ARGS(3, PyLong_FromSsize_t, Py_ssize_t, a, b, c);
460+
RETURN_PACKED_ARGS(7, PyLong_FromSsize_t, Py_ssize_t, a, b, c, d, e, f, g);
456461
}
457462

458463

Modules/clinic/_testclinic.c.h

Lines changed: 63 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/modsupport.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ _Py_convert_optional_to_ssize_t(PyObject *obj, void *result)
3333
return 1;
3434
}
3535

36+
int
37+
_Py_convert_optional_to_non_negative_ssize_t(PyObject *obj, void *result)
38+
{
39+
if (!_Py_convert_optional_to_ssize_t(obj, result)) {
40+
return 0;
41+
}
42+
if (obj != Py_None && *((Py_ssize_t *)result) < 0) {
43+
PyErr_SetString(PyExc_ValueError, "argument cannot be negative");
44+
return 0;
45+
}
46+
return 1;
47+
}
48+
3649

3750
/* Helper for mkvalue() to scan the length of a format */
3851

Tools/clinic/libclinic/converters.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -420,21 +420,39 @@ class Py_ssize_t_converter(CConverter):
420420
type = 'Py_ssize_t'
421421
c_ignored_default = "0"
422422

423-
def converter_init(self, *, accept: TypeSet = {int}) -> None:
423+
def converter_init(self, *, accept: TypeSet = {int},
424+
allow_negative: bool = True) -> None:
425+
self.allow_negative = allow_negative
424426
if accept == {int}:
425427
self.format_unit = 'n'
426428
self.default_type = int
427429
elif accept == {int, NoneType}:
428-
self.converter = '_Py_convert_optional_to_ssize_t'
430+
if self.allow_negative:
431+
self.converter = '_Py_convert_optional_to_ssize_t'
432+
else:
433+
self.converter = '_Py_convert_optional_to_non_negative_ssize_t'
429434
else:
430435
fail(f"Py_ssize_t_converter: illegal 'accept' argument {accept!r}")
431436

432437
def use_converter(self) -> None:
433-
if self.converter == '_Py_convert_optional_to_ssize_t':
434-
self.add_include('pycore_abstract.h',
435-
'_Py_convert_optional_to_ssize_t()')
438+
if self.converter in {
439+
'_Py_convert_optional_to_ssize_t',
440+
'_Py_convert_optional_to_non_negative_ssize_t',
441+
}:
442+
self.add_include('pycore_abstract.h', f'{self.converter}()')
436443

437444
def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
445+
if self.allow_negative:
446+
non_negative_check = ''
447+
else:
448+
non_negative_check = self.format_code("""
449+
if ({paramname} < 0) {{{{
450+
PyErr_SetString(PyExc_ValueError,
451+
"{paramname} cannot be negative");
452+
goto exit;
453+
}}}}""",
454+
argname=argname,
455+
)
438456
if self.format_unit == 'n':
439457
if limited_capi:
440458
PyNumber_Index = 'PyNumber_Index'
@@ -452,11 +470,13 @@ def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> st
452470
if (ival == -1 && PyErr_Occurred()) {{{{
453471
goto exit;
454472
}}}}
455-
{paramname} = ival;
473+
{paramname} = ival;{non_negative_check}
456474
}}}}
457475
""",
458476
argname=argname,
459-
PyNumber_Index=PyNumber_Index)
477+
PyNumber_Index=PyNumber_Index,
478+
non_negative_check=non_negative_check,
479+
)
460480
if not limited_capi:
461481
return super().parse_arg(argname, displayname, limited_capi=limited_capi)
462482
return self.format_code("""
@@ -465,7 +485,7 @@ def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> st
465485
{paramname} = PyNumber_AsSsize_t({argname}, PyExc_OverflowError);
466486
if ({paramname} == -1 && PyErr_Occurred()) {{{{
467487
goto exit;
468-
}}}}
488+
}}}}{non_negative_check}
469489
}}}}
470490
else {{{{
471491
{bad_argument}
@@ -475,6 +495,7 @@ def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> st
475495
""",
476496
argname=argname,
477497
bad_argument=self.bad_argument(displayname, 'integer or None', limited_capi=limited_capi),
498+
non_negative_check=non_negative_check,
478499
)
479500

480501

0 commit comments

Comments
 (0)