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:
parent
2a886412ba
commit
83a9f48699
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue