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). +``$`` + :c:func:`PyArg_ParseTupleAndKeywords` only: + Indicates that the remaining arguments in the Python argument list are + keyword-only. Currently, all keyword-only arguments must also be optional + arguments, so ``|`` must always be specified before ``$`` in the format string. + ``:`` 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 @@ -293,6 +293,77 @@ 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 + with self.assertRaisesRegex(TypeError, + "Required argument 'required' \(pos 1\) not found"): + getargs_keyword_only(optional=2) + + with self.assertRaisesRegex(TypeError, + "Required argument 'required' \(pos 1\) not found"): + getargs_keyword_only(keyword_only=3) + + def test_too_many_args(self): + with self.assertRaisesRegex(TypeError, + "Function takes at most 2 positional arguments \(3 given\)"): + getargs_keyword_only(1, 2, 3) + + with self.assertRaisesRegex(TypeError, + "function takes at most 3 arguments \(4 given\)"): + getargs_keyword_only(1, 2, 3, keyword_only=5) + + def test_invalid_keyword(self): + # extraneous keyword arg + with self.assertRaisesRegex(TypeError, + "'monster' is an invalid keyword argument for this function"): + getargs_keyword_only(1, 2, monster=666) + + def test_surrogate_keyword(self): + with self.assertRaisesRegex(TypeError, + "'\udc80' is an invalid keyword argument for this function"): + getargs_keyword_only(1, 2, **{'\uDC80': 10}) + class Bytes_TestCase(unittest.TestCase): def test_c(self): from _testcapi import getargs_c @@ -441,6 +512,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 @@ -801,7 +801,8 @@ } /* test PyArg_ParseTupleAndKeywords */ -static PyObject *getargs_keywords(PyObject *self, PyObject *args, PyObject *kwargs) +static PyObject * +getargs_keywords(PyObject *self, PyObject *args, PyObject *kwargs) { static char *keywords[] = {"arg1","arg2","arg3","arg4","arg5", NULL}; static char *fmt="(ii)i|(i(ii))(iii)i"; @@ -816,6 +817,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}; + int required = -1; + int optional = -1; + int keyword_only = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|i$i", 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 +2416,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 @@ -1403,6 +1403,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 *current_arg; freelist_t freelist = {0, NULL}; @@ -1452,8 +1453,39 @@ for (i = 0; i < len; i++) { keyword = kwlist[i]; if (*format == '|') { + if (min != INT_MAX) { + PyErr_SetString(PyExc_RuntimeError, + "Invalid format string (| specified twice)"); + return cleanreturn(0, &freelist); + } + min = i; format++; + + if (max != INT_MAX) { + PyErr_SetString(PyExc_RuntimeError, + "Invalid format string ($ before |)"); + return cleanreturn(0, &freelist); + } + } + if (*format == '$') { + if (max != INT_MAX) { + PyErr_SetString(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, @@ -1514,7 +1546,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);