Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 83a9f48

Browse files
committed
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.
1 parent 2a88641 commit 83a9f48

4 files changed

Lines changed: 134 additions & 3 deletions

File tree

Doc/c-api/arg.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,15 @@ inside nested parentheses. They are:
338338
:c:func:`PyArg_ParseTuple` does not touch the contents of the corresponding C
339339
variable(s).
340340

341+
``$``
342+
:c:func:`PyArg_ParseTupleAndKeywords` only:
343+
Indicates that the remaining arguments in the Python argument list are
344+
keyword-only. Currently, all keyword-only arguments must also be optional
345+
arguments, so ``|`` must always be specified before ``$`` in the format
346+
string.
347+
348+
.. versionadded:: 3.3
349+
341350
``:``
342351
The list of format units ends here; the string after the colon is used as the
343352
function name in error messages (the "associated value" of the exception that

Lib/test/test_getargs2.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22
from test import support
3-
from _testcapi import getargs_keywords
3+
from _testcapi import getargs_keywords, getargs_keyword_only
44

55
"""
66
> How about the following counterproposal. This also changes some of
@@ -293,6 +293,77 @@ def test_surrogate_keyword(self):
293293
else:
294294
self.fail('TypeError should have been raised')
295295

296+
class KeywordOnly_TestCase(unittest.TestCase):
297+
def test_positional_args(self):
298+
# using all possible positional args
299+
self.assertEqual(
300+
getargs_keyword_only(1, 2),
301+
(1, 2, -1)
302+
)
303+
304+
def test_mixed_args(self):
305+
# positional and keyword args
306+
self.assertEqual(
307+
getargs_keyword_only(1, 2, keyword_only=3),
308+
(1, 2, 3)
309+
)
310+
311+
def test_keyword_args(self):
312+
# all keywords
313+
self.assertEqual(
314+
getargs_keyword_only(required=1, optional=2, keyword_only=3),
315+
(1, 2, 3)
316+
)
317+
318+
def test_optional_args(self):
319+
# missing optional keyword args, skipping tuples
320+
self.assertEqual(
321+
getargs_keyword_only(required=1, optional=2),
322+
(1, 2, -1)
323+
)
324+
self.assertEqual(
325+
getargs_keyword_only(required=1, keyword_only=3),
326+
(1, -1, 3)
327+
)
328+
329+
def test_required_args(self):
330+
self.assertEqual(
331+
getargs_keyword_only(1),
332+
(1, -1, -1)
333+
)
334+
self.assertEqual(
335+
getargs_keyword_only(required=1),
336+
(1, -1, -1)
337+
)
338+
# required arg missing
339+
with self.assertRaisesRegex(TypeError,
340+
"Required argument 'required' \(pos 1\) not found"):
341+
getargs_keyword_only(optional=2)
342+
343+
with self.assertRaisesRegex(TypeError,
344+
"Required argument 'required' \(pos 1\) not found"):
345+
getargs_keyword_only(keyword_only=3)
346+
347+
def test_too_many_args(self):
348+
with self.assertRaisesRegex(TypeError,
349+
"Function takes at most 2 positional arguments \(3 given\)"):
350+
getargs_keyword_only(1, 2, 3)
351+
352+
with self.assertRaisesRegex(TypeError,
353+
"function takes at most 3 arguments \(4 given\)"):
354+
getargs_keyword_only(1, 2, 3, keyword_only=5)
355+
356+
def test_invalid_keyword(self):
357+
# extraneous keyword arg
358+
with self.assertRaisesRegex(TypeError,
359+
"'monster' is an invalid keyword argument for this function"):
360+
getargs_keyword_only(1, 2, monster=666)
361+
362+
def test_surrogate_keyword(self):
363+
with self.assertRaisesRegex(TypeError,
364+
"'\udc80' is an invalid keyword argument for this function"):
365+
getargs_keyword_only(1, 2, **{'\uDC80': 10})
366+
296367
class Bytes_TestCase(unittest.TestCase):
297368
def test_c(self):
298369
from _testcapi import getargs_c
@@ -441,6 +512,7 @@ def test_main():
441512
Unsigned_TestCase,
442513
Tuple_TestCase,
443514
Keywords_TestCase,
515+
KeywordOnly_TestCase,
444516
Bytes_TestCase,
445517
Unicode_TestCase,
446518
]

Modules/_testcapimodule.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,8 @@ getargs_tuple(PyObject *self, PyObject *args)
801801
}
802802

803803
/* test PyArg_ParseTupleAndKeywords */
804-
static PyObject *getargs_keywords(PyObject *self, PyObject *args, PyObject *kwargs)
804+
static PyObject *
805+
getargs_keywords(PyObject *self, PyObject *args, PyObject *kwargs)
805806
{
806807
static char *keywords[] = {"arg1","arg2","arg3","arg4","arg5", NULL};
807808
static char *fmt="(ii)i|(i(ii))(iii)i";
@@ -816,6 +817,21 @@ static PyObject *getargs_keywords(PyObject *self, PyObject *args, PyObject *kwar
816817
int_args[5], int_args[6], int_args[7], int_args[8], int_args[9]);
817818
}
818819

820+
/* test PyArg_ParseTupleAndKeywords keyword-only arguments */
821+
static PyObject *
822+
getargs_keyword_only(PyObject *self, PyObject *args, PyObject *kwargs)
823+
{
824+
static char *keywords[] = {"required", "optional", "keyword_only", NULL};
825+
int required = -1;
826+
int optional = -1;
827+
int keyword_only = -1;
828+
829+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|i$i", keywords,
830+
&required, &optional, &keyword_only))
831+
return NULL;
832+
return Py_BuildValue("iii", required, optional, keyword_only);
833+
}
834+
819835
/* Functions to call PyArg_ParseTuple with integer format codes,
820836
and return the result.
821837
*/
@@ -2400,6 +2416,8 @@ static PyMethodDef TestMethods[] = {
24002416
{"getargs_tuple", getargs_tuple, METH_VARARGS},
24012417
{"getargs_keywords", (PyCFunction)getargs_keywords,
24022418
METH_VARARGS|METH_KEYWORDS},
2419+
{"getargs_keyword_only", (PyCFunction)getargs_keyword_only,
2420+
METH_VARARGS|METH_KEYWORDS},
24032421
{"getargs_b", getargs_b, METH_VARARGS},
24042422
{"getargs_B", getargs_B, METH_VARARGS},
24052423
{"getargs_h", getargs_h, METH_VARARGS},

Python/getargs.c

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,7 @@ vgetargskeywords(PyObject *args, PyObject *keywords, const char *format,
14031403
int levels[32];
14041404
const char *fname, *msg, *custom_msg, *keyword;
14051405
int min = INT_MAX;
1406+
int max = INT_MAX;
14061407
int i, len, nargs, nkeywords;
14071408
PyObject *current_arg;
14081409
freelist_t freelist = {0, NULL};
@@ -1452,8 +1453,39 @@ vgetargskeywords(PyObject *args, PyObject *keywords, const char *format,
14521453
for (i = 0; i < len; i++) {
14531454
keyword = kwlist[i];
14541455
if (*format == '|') {
1456+
if (min != INT_MAX) {
1457+
PyErr_SetString(PyExc_RuntimeError,
1458+
"Invalid format string (| specified twice)");
1459+
return cleanreturn(0, &freelist);
1460+
}
1461+
14551462
min = i;
14561463
format++;
1464+
1465+
if (max != INT_MAX) {
1466+
PyErr_SetString(PyExc_RuntimeError,
1467+
"Invalid format string ($ before |)");
1468+
return cleanreturn(0, &freelist);
1469+
}
1470+
}
1471+
if (*format == '$') {
1472+
if (max != INT_MAX) {
1473+
PyErr_SetString(PyExc_RuntimeError,
1474+
"Invalid format string ($ specified twice)");
1475+
return cleanreturn(0, &freelist);
1476+
}
1477+
1478+
max = i;
1479+
format++;
1480+
1481+
if (max < nargs) {
1482+
PyErr_Format(PyExc_TypeError,
1483+
"Function takes %s %d positional arguments"
1484+
" (%d given)",
1485+
(min != INT_MAX) ? "at most" : "exactly",
1486+
max, nargs);
1487+
return cleanreturn(0, &freelist);
1488+
}
14571489
}
14581490
if (IS_END_OF_FORMAT(*format)) {
14591491
PyErr_Format(PyExc_RuntimeError,
@@ -1514,7 +1546,7 @@ vgetargskeywords(PyObject *args, PyObject *keywords, const char *format,
15141546
}
15151547
}
15161548

1517-
if (!IS_END_OF_FORMAT(*format) && *format != '|') {
1549+
if (!IS_END_OF_FORMAT(*format) && (*format != '|') && (*format != '$')) {
15181550
PyErr_Format(PyExc_RuntimeError,
15191551
"more argument specifiers than keyword list entries "
15201552
"(remaining format:'%s')", format);

0 commit comments

Comments
 (0)