summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLarry Hastings <larry@hastings.org>2012-03-20 20:06:16 +0000
committerLarry Hastings <larry@hastings.org>2012-03-20 20:06:16 +0000
commitc067da0e5c050df338fdc2f8ab6a01283f92f0ea (patch)
tree3f2c3e7d841c3a64d57e43936a025e76a5055616
parent5409615e74558e99b8f4b6575a6d78f5a1d7a840 (diff)
downloadcpython-c067da0e5c050df338fdc2f8ab6a01283f92f0ea.tar.gz
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.
-rw-r--r--Doc/c-api/arg.rst9
-rw-r--r--Lib/test/test_getargs2.py74
-rw-r--r--Modules/_testcapimodule.c20
-rw-r--r--Python/getargs.c34
4 files changed, 134 insertions, 3 deletions
diff --git a/Doc/c-api/arg.rst b/Doc/c-api/arg.rst
index 196aa772b8..f33714e1b7 100644
--- a/Doc/c-api/arg.rst
+++ b/Doc/c-api/arg.rst
@@ -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
diff --git a/Lib/test/test_getargs2.py b/Lib/test/test_getargs2.py
index 768ea8d486..fe1e7ce50e 100644
--- 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 @@ 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,
]
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index 9cafa739ac..093f205dfd 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -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},
diff --git a/Python/getargs.c b/Python/getargs.c
index 38c9dde6ff..8ec7110610 100644
--- a/Python/getargs.c
+++ b/Python/getargs.c
@@ -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);