]> granicus.if.org Git - python/commitdiff
Issue #14328: Add keyword-only parameters to PyArg_ParseTupleAndKeywords.
authorLarry Hastings <larry@hastings.org>
Tue, 20 Mar 2012 20:06:16 +0000 (20:06 +0000)
committerLarry Hastings <larry@hastings.org>
Tue, 20 Mar 2012 20:06:16 +0000 (20:06 +0000)
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.

Doc/c-api/arg.rst
Lib/test/test_getargs2.py
Modules/_testcapimodule.c
Python/getargs.c

index 196aa772b8c04728c946c9bdbefc76756c962219..f33714e1b72a850acd2492b2324d4c2b4a625f69 100644 (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
index 768ea8d4865d1f3d392035f2cd9e2db948081190..fe1e7ce50e43e15686f58ac9b333b165d23b2c97 100644 (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,
     ]
index 9cafa739ac27ef33bcfec1cb6eaf3b235c888ab4..093f205dfdf06d373e52e3b0842e182a9a7401dd 100644 (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},
index 38c9dde6ff71760be3a215c198608b0a1ac5487d..8ec711061049ba09b96e09d36a0a30667d3688de 100644 (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);