diff --git a/Doc/c-api/arg.rst b/Doc/c-api/arg.rst --- a/Doc/c-api/arg.rst +++ b/Doc/c-api/arg.rst @@ -338,6 +338,12 @@ :c:func:`PyArg_ParseTuple` does not touch the contents of the corresponding C variable(s). +``$`` + Indicates that the remaining arguments in the Python argument list are + keyword-only. All keyword-only arguments must also be optional arguments, + which means that ``|`` must also be present in the format string, + and ``$`` must be specified after ``|``. + ``:`` The list of format units ends here; the string after the colon is used as the function name in error messages (the "associated value" of the exception that diff --git a/Lib/test/test_getargs2.py b/Lib/test/test_getargs2.py --- a/Lib/test/test_getargs2.py +++ b/Lib/test/test_getargs2.py @@ -1,6 +1,6 @@ import unittest from test import support -from _testcapi import getargs_keywords +from _testcapi import getargs_keywords, getargs_keyword_only """ > How about the following counterproposal. This also changes some of @@ -272,7 +272,7 @@ try: getargs_keywords((1,2),3,(4,(5,6)),(7,8,9),10,111) except TypeError as err: - self.assertEqual(str(err), "function takes at most 5 arguments (6 given)") + self.assertEqual(str(err), "Function takes at most 5 arguments (6 given)") else: self.fail('TypeError should have been raised') @@ -293,6 +293,95 @@ else: self.fail('TypeError should have been raised') +class KeywordOnly_TestCase(unittest.TestCase): + def test_positional_args(self): + # using all possible positional args + self.assertEqual( + getargs_keyword_only(1, 2), + (1, 2, -1) + ) + + def test_mixed_args(self): + # positional and keyword args + self.assertEqual( + getargs_keyword_only(1, 2, keyword_only=3), + (1, 2, 3) + ) + + def test_keyword_args(self): + # all keywords + self.assertEqual( + getargs_keyword_only(required=1, optional=2, keyword_only=3), + (1, 2, 3) + ) + + def test_optional_args(self): + # missing optional keyword args, skipping tuples + self.assertEqual( + getargs_keyword_only(required=1, optional=2), + (1, 2, -1) + ) + self.assertEqual( + getargs_keyword_only(required=1, keyword_only=3), + (1, -1, 3) + ) + + def test_required_args(self): + self.assertEqual( + getargs_keyword_only(1), + (1, -1, -1) + ) + self.assertEqual( + getargs_keyword_only(required=1), + (1, -1, -1) + ) + # required arg missing + try: + getargs_keyword_only(optional=2) + except TypeError as err: + self.assertEqual(str(err), "Required argument 'required' (pos 1) not found") + else: + self.fail('TypeError should have been raised') + + try: + getargs_keyword_only(keyword_only=3) + except TypeError as err: + self.assertEqual(str(err), "Required argument 'required' (pos 1) not found") + else: + self.fail('TypeError should have been raised') + + def test_too_many_args(self): + try: + getargs_keyword_only(1, 2, 3) + except TypeError as err: + self.assertEqual(str(err), "Function takes at most 2 positional arguments (3 given)") + else: + self.fail('TypeError should have been raised') + + try: + getargs_keyword_only(1, 2, 3, keyword_only=5) + except TypeError as err: + self.assertEqual(str(err), "Function takes at most 3 arguments (4 given)") + else: + self.fail('TypeError should have been raised') + + def test_invalid_keyword(self): + # extraneous keyword arg + try: + getargs_keyword_only(1, 2, monster=666) + except TypeError as err: + self.assertEqual(str(err), "'monster' is an invalid keyword argument for this function") + else: + self.fail('TypeError should have been raised') + + def test_surrogate_keyword(self): + try: + getargs_keyword_only(1, 2, **{'\uDC80': 10}) + except TypeError as err: + self.assertEqual(str(err), "'\udc80' is an invalid keyword argument for this function") + else: + self.fail('TypeError should have been raised') + class Bytes_TestCase(unittest.TestCase): def test_c(self): from _testcapi import getargs_c @@ -441,6 +530,7 @@ Unsigned_TestCase, Tuple_TestCase, Keywords_TestCase, + KeywordOnly_TestCase, Bytes_TestCase, Unicode_TestCase, ] diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -816,6 +816,21 @@ int_args[5], int_args[6], int_args[7], int_args[8], int_args[9]); } +/* test PyArg_ParseTupleAndKeywords keyword-only arguments */ +static PyObject *getargs_keyword_only(PyObject *self, PyObject *args, PyObject *kwargs) +{ + static char *keywords[] = {"required", "optional", "keyword_only", NULL}; + static char *fmt="i|i$i"; + int required = -1; + int optional = -1; + int keyword_only = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, fmt, keywords, + &required, &optional, &keyword_only)) + return NULL; + return Py_BuildValue("iii", required, optional, keyword_only); +} + /* Functions to call PyArg_ParseTuple with integer format codes, and return the result. */ @@ -2400,6 +2415,8 @@ {"getargs_tuple", getargs_tuple, METH_VARARGS}, {"getargs_keywords", (PyCFunction)getargs_keywords, METH_VARARGS|METH_KEYWORDS}, + {"getargs_keyword_only", (PyCFunction)getargs_keyword_only, + METH_VARARGS|METH_KEYWORDS}, {"getargs_b", getargs_b, METH_VARARGS}, {"getargs_B", getargs_B, METH_VARARGS}, {"getargs_h", getargs_h, METH_VARARGS}, diff --git a/Python/getargs.c b/Python/getargs.c --- a/Python/getargs.c +++ b/Python/getargs.c @@ -247,6 +247,7 @@ int max = 0; int level = 0; int endfmt = 0; + int max_inc = 1; const char *formatsave = format; Py_ssize_t i, len; char *msg; @@ -261,7 +262,7 @@ switch (c) { case '(': if (level == 0) - max++; + max += max_inc; level++; if (level >= 30) Py_FatalError("too many tuple nesting levels " @@ -287,12 +288,15 @@ default: if (level == 0) { if (c == 'O') - max++; + max += max_inc; else if (isalpha(Py_CHARMASK(c))) { if (c != 'e') /* skip encoded */ - max++; + max += max_inc; } else if (c == '|') min = max; + else if (c == '$') { + max_inc = 0; + } } break; } @@ -363,7 +367,7 @@ } for (i = 0; i < len; i++) { - if (*format == '|') + if ((*format == '|') || (*format == '$')) format++; msg = convertitem(PyTuple_GET_ITEM(args, i), &format, p_va, flags, levels, msgbuf, @@ -376,7 +380,8 @@ if (*format != '\0' && !isalpha(Py_CHARMASK(*format)) && *format != '(' && - *format != '|' && *format != ':' && *format != ';') { + *format != '|' && *format != '$' && + *format != ':' && *format != ';') { PyErr_Format(PyExc_SystemError, "bad format string: %.200s", formatsave); return cleanreturn(0, freelist); @@ -1441,6 +1446,7 @@ int levels[32]; const char *fname, *msg, *custom_msg, *keyword; int min = INT_MAX; + int max = INT_MAX; int i, len, nargs, nkeywords; PyObject *freelist = NULL, *current_arg; @@ -1471,7 +1477,7 @@ if (nargs + nkeywords > len) { PyErr_Format(PyExc_TypeError, "%s%s takes at most %d argument%s (%d given)", - (fname == NULL) ? "function" : fname, + (fname == NULL) ? "Function" : fname, (fname == NULL) ? "" : "()", len, (len == 1) ? "" : "s", @@ -1483,8 +1489,39 @@ for (i = 0; i < len; i++) { keyword = kwlist[i]; if (*format == '|') { + if (min != INT_MAX) { + PyErr_Format(PyExc_RuntimeError, + "Invalid format string (| specified twice)"); + return cleanreturn(0, freelist); + } + min = i; format++; + + if (max != INT_MAX) { + PyErr_Format(PyExc_RuntimeError, + "Invalid format string ($ before |)"); + return cleanreturn(0, freelist); + } + } + if (*format == '$') { + if (max != INT_MAX) { + PyErr_Format(PyExc_RuntimeError, + "Invalid format string ($ specified twice)"); + return cleanreturn(0, freelist); + } + + max = i; + format++; + + if (max < nargs) { + PyErr_Format(PyExc_TypeError, + "Function takes %s %d positional arguments" + " (%d given)", + (min != INT_MAX) ? "at most" : "exactly", + max, nargs); + return cleanreturn(0, freelist); + } } if (IS_END_OF_FORMAT(*format)) { PyErr_Format(PyExc_RuntimeError, @@ -1545,7 +1582,7 @@ } } - if (!IS_END_OF_FORMAT(*format) && *format != '|') { + if (!IS_END_OF_FORMAT(*format) && (*format != '|') && (*format != '$')) { PyErr_Format(PyExc_RuntimeError, "more argument specifiers than keyword list entries " "(remaining format:'%s')", format);