Skip to content

Commit 548ce09

Browse files
gh-110815: Improve tests for PyArg_ParseTupleAndKeywords() (GH-110817)
1 parent 2ab34f0 commit 548ce09

File tree

2 files changed

+70
-18
lines changed

2 files changed

+70
-18
lines changed

Lib/test/test_capi/test_getargs.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
LLONG_MIN = -2**63
5656
ULLONG_MAX = 2**64-1
5757

58+
NULL = None
59+
5860
class Index:
5961
def __index__(self):
6062
return 99
@@ -1160,6 +1162,27 @@ def test_parse_tuple_and_keywords(self):
11601162
self.assertRaises(ValueError, _testcapi.parse_tuple_and_keywords,
11611163
(), {}, '', [42])
11621164

1165+
def test_basic(self):
1166+
parse = _testcapi.parse_tuple_and_keywords
1167+
1168+
self.assertEqual(parse((), {'a': 1}, 'O', ['a']), (1,))
1169+
self.assertEqual(parse((), {}, '|O', ['a']), (NULL,))
1170+
self.assertEqual(parse((1, 2), {}, 'OO', ['a', 'b']), (1, 2))
1171+
self.assertEqual(parse((1,), {'b': 2}, 'OO', ['a', 'b']), (1, 2))
1172+
self.assertEqual(parse((), {'a': 1, 'b': 2}, 'OO', ['a', 'b']), (1, 2))
1173+
self.assertEqual(parse((), {'b': 2}, '|OO', ['a', 'b']), (NULL, 2))
1174+
1175+
with self.assertRaisesRegex(TypeError,
1176+
"function missing required argument 'a'"):
1177+
parse((), {}, 'O', ['a'])
1178+
with self.assertRaisesRegex(TypeError,
1179+
"'b' is an invalid keyword argument"):
1180+
parse((), {'b': 1}, '|O', ['a'])
1181+
with self.assertRaisesRegex(TypeError,
1182+
fr"argument for function given by name \('a'\) "
1183+
fr"and position \(1\)"):
1184+
parse((1,), {'a': 2}, 'O|O', ['a', 'b'])
1185+
11631186
def test_bad_use(self):
11641187
# Test handling invalid format and keywords in
11651188
# PyArg_ParseTupleAndKeywords()
@@ -1187,20 +1210,23 @@ def test_bad_use(self):
11871210
def test_positional_only(self):
11881211
parse = _testcapi.parse_tuple_and_keywords
11891212

1190-
parse((1, 2, 3), {}, 'OOO', ['', '', 'a'])
1191-
parse((1, 2), {'a': 3}, 'OOO', ['', '', 'a'])
1213+
self.assertEqual(parse((1, 2, 3), {}, 'OOO', ['', '', 'a']), (1, 2, 3))
1214+
self.assertEqual(parse((1, 2), {'a': 3}, 'OOO', ['', '', 'a']), (1, 2, 3))
11921215
with self.assertRaisesRegex(TypeError,
11931216
r'function takes at least 2 positional arguments \(1 given\)'):
11941217
parse((1,), {'a': 3}, 'OOO', ['', '', 'a'])
1195-
parse((1,), {}, 'O|OO', ['', '', 'a'])
1218+
self.assertEqual(parse((1,), {}, 'O|OO', ['', '', 'a']),
1219+
(1, NULL, NULL))
11961220
with self.assertRaisesRegex(TypeError,
11971221
r'function takes at least 1 positional argument \(0 given\)'):
11981222
parse((), {}, 'O|OO', ['', '', 'a'])
1199-
parse((1, 2), {'a': 3}, 'OO$O', ['', '', 'a'])
1223+
self.assertEqual(parse((1, 2), {'a': 3}, 'OO$O', ['', '', 'a']),
1224+
(1, 2, 3))
12001225
with self.assertRaisesRegex(TypeError,
12011226
r'function takes exactly 2 positional arguments \(1 given\)'):
12021227
parse((1,), {'a': 3}, 'OO$O', ['', '', 'a'])
1203-
parse((1,), {}, 'O|O$O', ['', '', 'a'])
1228+
self.assertEqual(parse((1,), {}, 'O|O$O', ['', '', 'a']),
1229+
(1, NULL, NULL))
12041230
with self.assertRaisesRegex(TypeError,
12051231
r'function takes at least 1 positional argument \(0 given\)'):
12061232
parse((), {}, 'O|O$O', ['', '', 'a'])

Modules/_testcapi/getargs.c

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ parse_tuple_and_keywords(PyObject *self, PyObject *args)
1313
const char *sub_format;
1414
PyObject *sub_keywords;
1515

16-
double buffers[8][4]; /* double ensures alignment where necessary */
17-
PyObject *converted[8];
18-
char *keywords[8 + 1]; /* space for NULL at end */
16+
#define MAX_PARAMS 8
17+
double buffers[MAX_PARAMS][4]; /* double ensures alignment where necessary */
18+
char *keywords[MAX_PARAMS + 1]; /* space for NULL at end */
1919

2020
PyObject *return_value = NULL;
2121

@@ -35,41 +35,67 @@ parse_tuple_and_keywords(PyObject *self, PyObject *args)
3535
}
3636

3737
memset(buffers, 0, sizeof(buffers));
38-
memset(converted, 0, sizeof(converted));
3938
memset(keywords, 0, sizeof(keywords));
4039

4140
Py_ssize_t size = PySequence_Fast_GET_SIZE(sub_keywords);
42-
if (size > 8) {
41+
if (size > MAX_PARAMS) {
4342
PyErr_SetString(PyExc_ValueError,
4443
"parse_tuple_and_keywords: too many keywords in sub_keywords");
4544
goto exit;
4645
}
4746

4847
for (Py_ssize_t i = 0; i < size; i++) {
4948
PyObject *o = PySequence_Fast_GET_ITEM(sub_keywords, i);
50-
if (!PyUnicode_FSConverter(o, (void *)(converted + i))) {
49+
if (PyUnicode_Check(o)) {
50+
keywords[i] = (char *)PyUnicode_AsUTF8(o);
51+
if (keywords[i] == NULL) {
52+
goto exit;
53+
}
54+
}
55+
else if (PyBytes_Check(o)) {
56+
keywords[i] = PyBytes_AS_STRING(o);
57+
}
58+
else {
5159
PyErr_Format(PyExc_ValueError,
5260
"parse_tuple_and_keywords: "
53-
"could not convert keywords[%zd] to narrow string", i);
61+
"keywords must be str or bytes", i);
5462
goto exit;
5563
}
56-
keywords[i] = PyBytes_AS_STRING(converted[i]);
5764
}
5865

66+
assert(MAX_PARAMS == 8);
5967
int result = PyArg_ParseTupleAndKeywords(sub_args, sub_kwargs,
6068
sub_format, keywords,
6169
buffers + 0, buffers + 1, buffers + 2, buffers + 3,
6270
buffers + 4, buffers + 5, buffers + 6, buffers + 7);
6371

6472
if (result) {
65-
return_value = Py_NewRef(Py_None);
73+
int objects_only = 1;
74+
for (const char *f = sub_format; *f; f++) {
75+
if (Py_ISALNUM(*f) && strchr("OSUY", *f) == NULL) {
76+
objects_only = 0;
77+
break;
78+
}
79+
}
80+
if (objects_only) {
81+
return_value = PyTuple_New(size);
82+
if (return_value == NULL) {
83+
goto exit;
84+
}
85+
for (Py_ssize_t i = 0; i < size; i++) {
86+
PyObject *arg = *(PyObject **)(buffers + i);
87+
if (arg == NULL) {
88+
arg = Py_None;
89+
}
90+
PyTuple_SET_ITEM(return_value, i, Py_NewRef(arg));
91+
}
92+
}
93+
else {
94+
return_value = Py_NewRef(Py_None);
95+
}
6696
}
6797

6898
exit:
69-
size = sizeof(converted) / sizeof(converted[0]);
70-
for (Py_ssize_t i = 0; i < size; i++) {
71-
Py_XDECREF(converted[i]);
72-
}
7399
return return_value;
74100
}
75101

0 commit comments

Comments
 (0)