From b7699fb102adc17afca11fda3cb632d414f4c8f1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 12 Apr 2024 18:36:39 +0300 Subject: [PATCH 1/3] C API: Add support of nullable arguments in PyArg_Parse --- Modules/_ctypes/_ctypes.c | 11 ++-- Modules/_json.c | 13 ++--- Modules/_threadmodule.c | 14 ++--- Modules/mmapmodule.c | 3 +- Python/getargs.c | 107 +++++++++++++++++++++++--------------- 5 files changed, 76 insertions(+), 72 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 1ff108a39320cf..7d8d550d0cfc7c 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3620,8 +3620,7 @@ _validate_paramflags(ctypes_state *st, PyTypeObject *type, PyObject *paramflags) PyObject *name = Py_None; PyObject *defval; PyObject *typ; - if (!PyArg_ParseTuple(item, "i|OO", &flag, &name, &defval) || - !(name == Py_None || PyUnicode_Check(name))) + if (!PyArg_ParseTuple(item, "i|?UO", &flag, &name, &defval)) { PyErr_SetString(PyExc_TypeError, "paramflags must be a sequence of (int [,string [,value]]) tuples"); @@ -3686,10 +3685,8 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds) void *handle; PyObject *paramflags = NULL; - if (!PyArg_ParseTuple(args, "O|O", &ftuple, ¶mflags)) + if (!PyArg_ParseTuple(args, "O|?O", &ftuple, ¶mflags)) return NULL; - if (paramflags == Py_None) - paramflags = NULL; ftuple = PySequence_Tuple(ftuple); if (!ftuple) @@ -3804,10 +3801,8 @@ PyCFuncPtr_FromVtblIndex(PyTypeObject *type, PyObject *args, PyObject *kwds) GUID *iid = NULL; Py_ssize_t iid_len = 0; - if (!PyArg_ParseTuple(args, "is|Oz#", &index, &name, ¶mflags, &iid, &iid_len)) + if (!PyArg_ParseTuple(args, "is|?Oz#", &index, &name, ¶mflags, &iid, &iid_len)) return NULL; - if (paramflags == Py_None) - paramflags = NULL; ctypes_state *st = get_module_state_by_def(Py_TYPE(type)); if (!_validate_paramflags(st, type, paramflags)) { diff --git a/Modules/_json.c b/Modules/_json.c index c7fe1561bb1018..f53e461bc8e5bb 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1208,23 +1208,16 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; PyEncoderObject *s; - PyObject *markers, *defaultfn, *encoder, *indent, *key_separator; + PyObject *markers = Py_None, *defaultfn, *encoder, *indent, *key_separator; PyObject *item_separator; int sort_keys, skipkeys, allow_nan; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppp:make_encoder", kwlist, - &markers, &defaultfn, &encoder, &indent, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "?O!OOOUUppp:make_encoder", kwlist, + &PyDict_Type, &markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator, &sort_keys, &skipkeys, &allow_nan)) return NULL; - if (markers != Py_None && !PyDict_Check(markers)) { - PyErr_Format(PyExc_TypeError, - "make_encoder() argument 1 must be dict or None, " - "not %.200s", Py_TYPE(markers)->tp_name); - return NULL; - } - s = (PyEncoderObject *)type->tp_alloc(type, 0); if (s == NULL) return NULL; diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 39d309729d88b8..8b8896059bebe2 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1878,10 +1878,10 @@ thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs, PyObject *func = NULL; int daemon = 1; thread_module_state *state = get_thread_state(module); - PyObject *hobj = NULL; + PyObject *hobj = Py_None; if (!PyArg_ParseTupleAndKeywords(fargs, fkwargs, - "O|Op:start_joinable_thread", keywords, - &func, &hobj, &daemon)) { + "O|?O!p:start_joinable_thread", keywords, + &func, state->thread_handle_type, &hobj, &daemon)) { return NULL; } @@ -1891,14 +1891,6 @@ thread_PyThread_start_joinable_thread(PyObject *module, PyObject *fargs, return NULL; } - if (hobj == NULL) { - hobj = Py_None; - } - else if (hobj != Py_None && !Py_IS_TYPE(hobj, state->thread_handle_type)) { - PyErr_SetString(PyExc_TypeError, "'handle' must be a _ThreadHandle"); - return NULL; - } - if (PySys_Audit("_thread.start_joinable_thread", "OiO", func, daemon, hobj) < 0) { return NULL; diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c index 99a85e9e49ad47..d034b64e2cc11a 100644 --- a/Modules/mmapmodule.c +++ b/Modules/mmapmodule.c @@ -23,7 +23,6 @@ #endif #include -#include "pycore_abstract.h" // _Py_convert_optional_to_ssize_t() #include "pycore_bytesobject.h" // _PyBytes_Find() #include "pycore_fileutils.h" // _Py_stat_struct @@ -512,7 +511,7 @@ mmap_read_method(mmap_object *self, Py_ssize_t num_bytes = PY_SSIZE_T_MAX, remaining; CHECK_VALID(NULL); - if (!PyArg_ParseTuple(args, "|O&:read", _Py_convert_optional_to_ssize_t, &num_bytes)) + if (!PyArg_ParseTuple(args, "|?n:read", &num_bytes)) return NULL; CHECK_VALID(NULL); diff --git a/Python/getargs.c b/Python/getargs.c index b96ce3a22dae7c..7d1ad6fb710154 100644 --- a/Python/getargs.c +++ b/Python/getargs.c @@ -21,6 +21,7 @@ PyAPI_FUNC(int) _PyArg_VaParseTupleAndKeywords_SizeT(PyObject *, PyObject *, const char *, const char * const *, va_list); #define FLAG_COMPAT 1 +#define FLAG_NULLABLE 4 typedef int (*destr_t)(PyObject *, void *); @@ -487,8 +488,9 @@ converttuple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (!PySequence_Check(arg) || PyBytes_Check(arg)) { levels[0] = 0; PyOS_snprintf(msgbuf, bufsize, - "must be %d-item sequence, not %.50s", + "must be %d-item sequence%s, not %.50s", n, + (flags & FLAG_NULLABLE) ? " or None" : "", arg == Py_None ? "None" : Py_TYPE(arg)->tp_name); return msgbuf; } @@ -502,6 +504,7 @@ converttuple(PyObject *arg, const char **p_format, va_list *p_va, int flags, return msgbuf; } + flags &= ~FLAG_NULLABLE; format = *p_format; for (i = 0; i < n; i++) { const char *msg; @@ -538,6 +541,20 @@ convertitem(PyObject *arg, const char **p_format, va_list *p_va, int flags, const char *msg; const char *format = *p_format; + if (*format == '?') { + format++; + if (arg == Py_None) { + msg = skipitem(&format, p_va, flags); + if (msg == NULL) { + *p_format = format; + } + else { + levels[0] = 0; + } + return msg; + } + flags |= FLAG_NULLABLE; + } if (*format == '(' /* ')' */) { format++; msg = converttuple(arg, &format, p_va, flags, levels, msgbuf, @@ -573,7 +590,7 @@ _PyArg_BadArgument(const char *fname, const char *displayname, } static const char * -converterr(const char *expected, PyObject *arg, char *msgbuf, size_t bufsize) +converterr(int flags, const char *expected, PyObject *arg, char *msgbuf, size_t bufsize) { assert(expected != NULL); assert(arg != NULL); @@ -583,20 +600,25 @@ converterr(const char *expected, PyObject *arg, char *msgbuf, size_t bufsize) } else { PyOS_snprintf(msgbuf, bufsize, - "must be %.50s, not %.50s", expected, + "must be %.50s%s, not %.50s", expected, + ((flags & FLAG_NULLABLE) && !strstr(expected, " or None")) + ? " or None" : "", arg == Py_None ? "None" : Py_TYPE(arg)->tp_name); } return msgbuf; } static const char * -convertcharerr(const char *expected, const char *what, Py_ssize_t size, +convertcharerr(int flags, const char *expected, const char *what, Py_ssize_t size, char *msgbuf, size_t bufsize) { assert(expected != NULL); PyOS_snprintf(msgbuf, bufsize, - "must be %.50s, not %.50s of length %zd", - expected, what, size); + "must be %.50s%s, not %.50s of length %zd", + expected, + ((flags & FLAG_NULLABLE) && !strstr(expected, " or None")) + ? " or None" : "", + what, size); return msgbuf; } @@ -747,7 +769,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (PyLong_Check(arg)) ival = PyLong_AsUnsignedLongMask(arg); else - return converterr("int", arg, msgbuf, bufsize); + return converterr(flags, "int", arg, msgbuf, bufsize); *p = ival; break; } @@ -768,7 +790,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (PyLong_Check(arg)) ival = PyLong_AsUnsignedLongLongMask(arg); else - return converterr("int", arg, msgbuf, bufsize); + return converterr(flags, "int", arg, msgbuf, bufsize); *p = ival; break; } @@ -808,7 +830,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, char *p = va_arg(*p_va, char *); if (PyBytes_Check(arg)) { if (PyBytes_GET_SIZE(arg) != 1) { - return convertcharerr("a byte string of length 1", + return convertcharerr(flags, "a byte string of length 1", "a bytes object", PyBytes_GET_SIZE(arg), msgbuf, bufsize); } @@ -816,14 +838,14 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, } else if (PyByteArray_Check(arg)) { if (PyByteArray_GET_SIZE(arg) != 1) { - return convertcharerr("a byte string of length 1", + return convertcharerr(flags, "a byte string of length 1", "a bytearray object", PyByteArray_GET_SIZE(arg), msgbuf, bufsize); } *p = PyByteArray_AS_STRING(arg)[0]; } else - return converterr("a byte string of length 1", arg, msgbuf, bufsize); + return converterr(flags, "a byte string of length 1", arg, msgbuf, bufsize); break; } @@ -833,10 +855,10 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, const void *data; if (!PyUnicode_Check(arg)) - return converterr("a unicode character", arg, msgbuf, bufsize); + return converterr(flags, "a unicode character", arg, msgbuf, bufsize); if (PyUnicode_GET_LENGTH(arg) != 1) { - return convertcharerr("a unicode character", + return convertcharerr(flags, "a unicode character", "a string", PyUnicode_GET_LENGTH(arg), msgbuf, bufsize); } @@ -868,18 +890,18 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, Py_ssize_t count; if (*format == '*') { if (getbuffer(arg, (Py_buffer*)p, &buf) < 0) - return converterr(buf, arg, msgbuf, bufsize); + return converterr(flags, buf, arg, msgbuf, bufsize); format++; if (addcleanup(p, freelist, cleanup_buffer)) { return converterr( - "(cleanup problem)", + flags, "(cleanup problem)", arg, msgbuf, bufsize); } break; } count = convertbuffer(arg, (const void **)p, &buf); if (count < 0) - return converterr(buf, arg, msgbuf, bufsize); + return converterr(flags, buf, arg, msgbuf, bufsize); if (*format == '#') { Py_ssize_t *psize = va_arg(*p_va, Py_ssize_t*); *psize = count; @@ -906,18 +928,18 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, Py_ssize_t len; sarg = PyUnicode_AsUTF8AndSize(arg, &len); if (sarg == NULL) - return converterr(CONV_UNICODE, + return converterr(flags, CONV_UNICODE, arg, msgbuf, bufsize); PyBuffer_FillInfo(p, arg, (void *)sarg, len, 1, 0); } else { /* any bytes-like object */ const char *buf; if (getbuffer(arg, p, &buf) < 0) - return converterr(buf, arg, msgbuf, bufsize); + return converterr(flags, buf, arg, msgbuf, bufsize); } if (addcleanup(p, freelist, cleanup_buffer)) { return converterr( - "(cleanup problem)", + flags, "(cleanup problem)", arg, msgbuf, bufsize); } format++; @@ -934,7 +956,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, Py_ssize_t len; sarg = PyUnicode_AsUTF8AndSize(arg, &len); if (sarg == NULL) - return converterr(CONV_UNICODE, + return converterr(flags, CONV_UNICODE, arg, msgbuf, bufsize); *p = sarg; *psize = len; @@ -944,7 +966,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, const char *buf; Py_ssize_t count = convertbuffer(arg, p, &buf); if (count < 0) - return converterr(buf, arg, msgbuf, bufsize); + return converterr(flags, buf, arg, msgbuf, bufsize); *psize = count; } format++; @@ -959,7 +981,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, else if (PyUnicode_Check(arg)) { sarg = PyUnicode_AsUTF8AndSize(arg, &len); if (sarg == NULL) - return converterr(CONV_UNICODE, + return converterr(flags, CONV_UNICODE, arg, msgbuf, bufsize); if (strlen(sarg) != (size_t)len) { PyErr_SetString(PyExc_ValueError, "embedded null character"); @@ -968,7 +990,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, *p = sarg; } else - return converterr(c == 'z' ? "str or None" : "str", + return converterr(flags, c == 'z' ? "str or None" : "str", arg, msgbuf, bufsize); } break; @@ -997,12 +1019,12 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, recode_strings = 0; else return converterr( - "(unknown parser marker combination)", + flags, "(unknown parser marker combination)", arg, msgbuf, bufsize); buffer = (char **)va_arg(*p_va, char **); format++; if (buffer == NULL) - return converterr("(buffer is NULL)", + return converterr(flags, "(buffer is NULL)", arg, msgbuf, bufsize); /* Encode object */ @@ -1024,7 +1046,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, encoding, NULL); if (s == NULL) - return converterr("(encoding failed)", + return converterr(flags, "(encoding failed)", arg, msgbuf, bufsize); assert(PyBytes_Check(s)); size = PyBytes_GET_SIZE(s); @@ -1034,7 +1056,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, } else { return converterr( - recode_strings ? "str" : "str, bytes or bytearray", + flags, recode_strings ? "str" : "str, bytes or bytearray", arg, msgbuf, bufsize); } @@ -1067,7 +1089,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (psize == NULL) { Py_DECREF(s); return converterr( - "(buffer_len is NULL)", + flags, "(buffer_len is NULL)", arg, msgbuf, bufsize); } if (*buffer == NULL) { @@ -1080,7 +1102,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (addcleanup(buffer, freelist, cleanup_ptr)) { Py_DECREF(s); return converterr( - "(cleanup problem)", + flags, "(cleanup problem)", arg, msgbuf, bufsize); } } else { @@ -1114,7 +1136,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if ((Py_ssize_t)strlen(ptr) != size) { Py_DECREF(s); return converterr( - "encoded string without null bytes", + flags, "encoded string without null bytes", arg, msgbuf, bufsize); } *buffer = PyMem_NEW(char, size + 1); @@ -1125,7 +1147,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, } if (addcleanup(buffer, freelist, cleanup_ptr)) { Py_DECREF(s); - return converterr("(cleanup problem)", + return converterr(flags, "(cleanup problem)", arg, msgbuf, bufsize); } memcpy(*buffer, ptr, size+1); @@ -1139,7 +1161,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (PyBytes_Check(arg)) *p = arg; else - return converterr("bytes", arg, msgbuf, bufsize); + return converterr(flags, "bytes", arg, msgbuf, bufsize); break; } @@ -1148,7 +1170,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (PyByteArray_Check(arg)) *p = arg; else - return converterr("bytearray", arg, msgbuf, bufsize); + return converterr(flags, "bytearray", arg, msgbuf, bufsize); break; } @@ -1158,7 +1180,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, *p = arg; } else - return converterr("str", arg, msgbuf, bufsize); + return converterr(flags, "str", arg, msgbuf, bufsize); break; } @@ -1172,7 +1194,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (PyType_IsSubtype(Py_TYPE(arg), type)) *p = arg; else - return converterr(type->tp_name, arg, msgbuf, bufsize); + return converterr(flags, type->tp_name, arg, msgbuf, bufsize); } else if (*format == '&') { @@ -1182,11 +1204,11 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, int res; format++; if (! (res = (*convert)(arg, addr))) - return converterr("(unspecified)", + return converterr(flags, "(unspecified)", arg, msgbuf, bufsize); if (res == Py_CLEANUP_SUPPORTED && addcleanup(addr, freelist, convert) == -1) - return converterr("(cleanup problem)", + return converterr(flags, "(cleanup problem)", arg, msgbuf, bufsize); } else { @@ -1202,7 +1224,7 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, if (*format != '*') return converterr( - "(invalid use of 'w' format character)", + flags, "(invalid use of 'w' format character)", arg, msgbuf, bufsize); format++; @@ -1211,20 +1233,20 @@ convertsimple(PyObject *arg, const char **p_format, va_list *p_va, int flags, result is C-contiguous with format 'B'. */ if (PyObject_GetBuffer(arg, (Py_buffer*)p, PyBUF_WRITABLE) < 0) { PyErr_Clear(); - return converterr("read-write bytes-like object", + return converterr(flags, "read-write bytes-like object", arg, msgbuf, bufsize); } assert(PyBuffer_IsContiguous((Py_buffer *)p, 'C')); if (addcleanup(p, freelist, cleanup_buffer)) { return converterr( - "(cleanup problem)", + flags, "(cleanup problem)", arg, msgbuf, bufsize); } break; } default: - return converterr("(impossible)", arg, msgbuf, bufsize); + return converterr(flags, "(impossible)", arg, msgbuf, bufsize); } @@ -2630,6 +2652,9 @@ skipitem(const char **p_format, va_list *p_va, int flags) const char *format = *p_format; char c = *format++; + if (c == '?') { + c = *format++; + } switch (c) { /* From 9eda73e762e65e39cc2695e8c147fbb4afa078a8 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 3 Jul 2024 10:08:28 +0300 Subject: [PATCH 2/3] Add tests. --- Lib/test/test_capi/test_getargs.py | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Lib/test/test_capi/test_getargs.py b/Lib/test/test_capi/test_getargs.py index 232aa2a80025dc..8cbaa841983700 100644 --- a/Lib/test/test_capi/test_getargs.py +++ b/Lib/test/test_capi/test_getargs.py @@ -1352,6 +1352,71 @@ def test_nested_tuple(self): "argument 1 must be sequence of length 1, not 0"): parse(((),), {}, '(' + f + ')', ['a']) + def test_nullable(self): + parse = _testcapi.parse_tuple_and_keywords + + def check(format, arg, allows_none=False): + # Because some format units (such as y*) require cleanup, + # we force the parsing code to perform the cleanup by adding + # an argument that always fails. + # By checking for an exception, we ensure that the parsing + # of the first argument was successful. + self.assertRaises(OverflowError, parse, + (arg, 256), {}, '?' + format + 'b', ['a', 'b']) + self.assertRaises(OverflowError, parse, + (None, 256), {}, '?' + format + 'b', ['a', 'b']) + self.assertRaises(OverflowError, parse, + (arg, 256), {}, format + 'b', ['a', 'b']) + self.assertRaises(OverflowError if allows_none else TypeError, parse, + (None, 256), {}, format + 'b', ['a', 'b']) + + check('b', 42) + check('B', 42) + check('h', 42) + check('H', 42) + check('i', 42) + check('I', 42) + check('n', 42) + check('l', 42) + check('k', 42) + check('L', 42) + check('K', 42) + check('f', 2.5) + check('d', 2.5) + check('D', 2.5j) + check('c', b'a') + check('C', 'a') + check('p', True, allows_none=True) + check('y', b'buffer') + check('y*', b'buffer') + check('y#', b'buffer') + check('s', 'string') + check('s*', 'string') + check('s#', 'string') + check('z', 'string', allows_none=True) + check('z*', 'string', allows_none=True) + check('z#', 'string', allows_none=True) + check('w*', bytearray(b'buffer')) + check('U', 'string') + check('S', b'bytes') + check('Y', bytearray(b'bytearray')) + check('O', object, allows_none=True) + + check('(OO)', (1, 2)) + self.assertEqual(parse((((1, 2), 3),), {}, '(?(OO)O)', ['a']), (1, 2, 3)) + self.assertEqual(parse(((None, 3),), {}, '(?(OO)O)', ['a']), (NULL, NULL, 3)) + self.assertEqual(parse((((1, 2), 3),), {}, '((OO)O)', ['a']), (1, 2, 3)) + self.assertRaises(TypeError, parse, ((None, 3),), {}, '((OO)O)', ['a']) + + parse((None,), {}, '?es', ['a']) + parse((None,), {}, '?es#', ['a']) + parse((None,), {}, '?et', ['a']) + parse((None,), {}, '?et#', ['a']) + parse((None,), {}, '?O!', ['a']) + parse((None,), {}, '?O&', ['a']) + + # TODO: More tests for es?, es#?, et?, et#?, O!, O& + @unittest.skipIf(_testinternalcapi is None, 'needs _testinternalcapi') def test_gh_119213(self): rc, out, err = script_helper.assert_python_ok("-c", """if True: From 2707238bb78cd0235e0e7d059e5b63079c8e94ce Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 3 Jul 2024 13:43:41 +0300 Subject: [PATCH 3/3] Add more tests. --- Lib/test/test_capi/test_getargs.py | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Lib/test/test_capi/test_getargs.py b/Lib/test/test_capi/test_getargs.py index 8cbaa841983700..ba6be36ef35c8c 100644 --- a/Lib/test/test_capi/test_getargs.py +++ b/Lib/test/test_capi/test_getargs.py @@ -1352,6 +1352,58 @@ def test_nested_tuple(self): "argument 1 must be sequence of length 1, not 0"): parse(((),), {}, '(' + f + ')', ['a']) + def test_specific_type_errors(self): + parse = _testcapi.parse_tuple_and_keywords + + def check(format, arg, expected, got='list'): + errmsg = f'must be {expected}, not {got}' + with self.assertRaisesRegex(TypeError, errmsg): + parse((arg,), {}, format, ['a']) + + check('k', [], 'int') + check('?k', [], 'int or None') + check('K', [], 'int') + check('?K', [], 'int or None') + check('c', [], 'a byte string of length 1') + check('?c', [], 'a byte string of length 1 or None') + check('c', b'abc', 'a byte string of length 1', + 'a bytes object of length 3') + check('?c', b'abc', 'a byte string of length 1 or None', + 'a bytes object of length 3') + check('c', bytearray(b'abc'), 'a byte string of length 1', + 'a bytearray object of length 3') + check('?c', bytearray(b'abc'), 'a byte string of length 1 or None', + 'a bytearray object of length 3') + check('C', [], 'a unicode character') + check('?C', [], 'a unicode character or None') + check('C', 'abc', 'a unicode character', + 'a string of length 3') + check('?C', 'abc', 'a unicode character or None', + 'a string of length 3') + check('s', [], 'str') + check('?s', [], 'str or None') + check('z', [], 'str or None') + check('?z', [], 'str or None') + check('es', [], 'str') + check('?es', [], 'str or None') + check('es#', [], 'str') + check('?es#', [], 'str or None') + check('et', [], 'str, bytes or bytearray') + check('?et', [], 'str, bytes or bytearray or None') + check('et#', [], 'str, bytes or bytearray') + check('?et#', [], 'str, bytes or bytearray or None') + check('w*', [], 'read-write bytes-like object') + check('?w*', [], 'read-write bytes-like object or None') + check('S', [], 'bytes') + check('?S', [], 'bytes or None') + check('U', [], 'str') + check('?U', [], 'str or None') + check('Y', [], 'bytearray') + check('?Y', [], 'bytearray or None') + check('(OO)', 42, '2-item sequence', 'int') + check('?(OO)', 42, '2-item sequence or None', 'int') + check('(OO)', (1, 2, 3), 'sequence of length 2', '3') + def test_nullable(self): parse = _testcapi.parse_tuple_and_keywords