Issue #14328: Add keyword-only parameters to PyArg_ParseTupleAndKeywords.

They're optional-only for now (unlike in pure Python) but that's all
I needed.  The syntax can easily be relaxed if we want to support
required keyword-only arguments for extension types in the future.
This commit is contained in:
Larry Hastings 2012-03-20 20:06:16 +00:00
parent 2a886412ba
commit 83a9f48699
4 changed files with 134 additions and 3 deletions

View File

@ -338,6 +338,15 @@ inside nested parentheses. They are:
: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.
.. versionadded:: 3.3
``:``
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

View File

@ -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 @@ class Keywords_TestCase(unittest.TestCase):
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 @@ def test_main():
Unsigned_TestCase,
Tuple_TestCase,
Keywords_TestCase,
KeywordOnly_TestCase,
Bytes_TestCase,
Unicode_TestCase,
]

View File

@ -801,7 +801,8 @@ getargs_tuple(PyObject *self, PyObject *args)
}
/* 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 @@ static PyObject *getargs_keywords(PyObject *self, PyObject *args, PyObject *kwar
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 @@ static PyMethodDef TestMethods[] = {
{"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},

View File

@ -1403,6 +1403,7 @@ vgetargskeywords(PyObject *args, PyObject *keywords, const char *format,
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 @@ vgetargskeywords(PyObject *args, PyObject *keywords, const char *format,
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 @@ vgetargskeywords(PyObject *args, PyObject *keywords, const char *format,
}
}
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);