diff --git a/Lib/test/test_capi/test_getargs.py b/Lib/test/test_capi/test_getargs.py index ea12ab196ba774..3d8768b8845f8d 100644 --- a/Lib/test/test_capi/test_getargs.py +++ b/Lib/test/test_capi/test_getargs.py @@ -1341,6 +1341,34 @@ class Test_testcapi(unittest.TestCase): for name in dir(_testcapi) if name.startswith('test_') and name.endswith('_code')) + def test_nested_tuple(self): + parse = _testcapi.parse_tuple_and_keywords + + self.assertEqual(parse(((1, 2, 3),), {}, '(OOO)', ['a']), (1, 2, 3)) + self.assertEqual(parse((1, (2, 3), 4), {}, 'O(OO)O', ['a', 'b', 'c']), + (1, 2, 3, 4)) + parse(((1, 2, 3),), {}, '(iii)', ['a']) + + with self.assertRaisesRegex(TypeError, + "argument 1 must be sequence of length 2, not 3"): + parse(((1, 2, 3),), {}, '(ii)', ['a']) + with self.assertRaisesRegex(TypeError, + "argument 1 must be sequence of length 2, not 1"): + parse(((1,),), {}, '(ii)', ['a']) + with self.assertRaisesRegex(TypeError, + "argument 1 must be 2-item sequence, not int"): + parse((1,), {}, '(ii)', ['a']) + with self.assertRaisesRegex(TypeError, + "argument 1 must be 2-item sequence, not bytes"): + parse((b'ab',), {}, '(ii)', ['a']) + + for f in 'es', 'et', 'es#', 'et#': + with self.assertRaises(LookupError): # empty encoding "" + parse((('a',),), {}, '(' + f + ')', ['a']) + with self.assertRaisesRegex(TypeError, + "argument 1 must be sequence of length 1, not 0"): + parse(((),), {}, '(' + f + ')', ['a']) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C API/2023-11-27-09-44-16.gh-issue-112438.GdNZiI.rst b/Misc/NEWS.d/next/C API/2023-11-27-09-44-16.gh-issue-112438.GdNZiI.rst new file mode 100644 index 00000000000000..113119efd6aebb --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-11-27-09-44-16.gh-issue-112438.GdNZiI.rst @@ -0,0 +1,2 @@ +Fix support of format units "es", "et", "es#", and "et#" in nested tuples in +:c:func:`PyArg_ParseTuple`-like functions. diff --git a/Modules/_testcapi/getargs.c b/Modules/_testcapi/getargs.c index fbfd6a46693381..2c1f1257099b38 100644 --- a/Modules/_testcapi/getargs.c +++ b/Modules/_testcapi/getargs.c @@ -73,18 +73,22 @@ parse_tuple_and_keywords(PyObject *self, PyObject *args) if (result) { int objects_only = 1; + int count = 0; for (const char *f = sub_format; *f; f++) { - if (Py_ISALNUM(*f) && strchr("OSUY", *f) == NULL) { - objects_only = 0; - break; + if (Py_ISALNUM(*f)) { + if (strchr("OSUY", *f) == NULL) { + objects_only = 0; + break; + } + count++; } } if (objects_only) { - return_value = PyTuple_New(size); + return_value = PyTuple_New(count); if (return_value == NULL) { goto exit; } - for (Py_ssize_t i = 0; i < size; i++) { + for (Py_ssize_t i = 0; i < count; i++) { PyObject *arg = *(PyObject **)(buffers + i); if (arg == NULL) { arg = Py_None; diff --git a/Python/getargs.c b/Python/getargs.c index 66dd90877fe6ff..066739f21fc467 100644 --- a/Python/getargs.c +++ b/Python/getargs.c @@ -522,7 +522,7 @@ converttuple(PyObject *arg, const char **p_format, va_list *p_va, int flags, } else if (c == ':' || c == ';' || c == '\0') break; - else if (level == 0 && Py_ISALPHA(c)) + else if (level == 0 && Py_ISALPHA(c) && c != 'e') n++; }