diff -r 3084914245d2 Doc/c-api/arg.rst --- a/Doc/c-api/arg.rst Mon Feb 08 20:34:49 2016 -0800 +++ b/Doc/c-api/arg.rst Tue Feb 09 15:51:57 2016 +0200 @@ -406,8 +406,13 @@ API Functions .. c:function:: int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...) Parse the parameters of a function that takes both positional and keyword - parameters into local variables. Returns true on success; on failure, it - returns false and raises the appropriate exception. + parameters into local variables. *keywords* is a NULL-terminated array of + keyword parameter names. Empty name denotes positional-only parameter. + Returns true on success; on failure, it returns false and raises the + appropriate exception. + + .. versionchanged:: 3.6 + Added support for positional-only parameters. .. c:function:: int PyArg_VaParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], va_list vargs) diff -r 3084914245d2 Lib/test/test_capi.py --- a/Lib/test/test_capi.py Mon Feb 08 20:34:49 2016 -0800 +++ b/Lib/test/test_capi.py Tue Feb 09 15:51:57 2016 +0200 @@ -521,6 +521,96 @@ class SkipitemTest(unittest.TestCase): self.assertRaises(ValueError, _testcapi.parse_tuple_and_keywords, (), {}, b'', [42]) + def test_positional_only(self): + #_testcapi.parse_tuple_and_keywords((1, 2), {}, b'OO', ['', 'a']) + #_testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'OO', ['', 'a']) + #with self.assertRaises(TypeError): + #_testcapi.parse_tuple_and_keywords((), {'': 1, 'a': 2}, b'OO', ['', 'a']) + #_testcapi.parse_tuple_and_keywords((1,), {}, b'O|O', ['', 'a']) + #_testcapi.parse_tuple_and_keywords((), {}, b'|OO', ['', 'a']) + #_testcapi.parse_tuple_and_keywords((), {}, b'|O$O', ['', 'a']) + #with self.assertRaises(TypeError): + #_testcapi.parse_tuple_and_keywords((), {}, b'O$O', ['', 'a']) + #_testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'|OO', ['', 'a']) + + + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'OO', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'O$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'O|O', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'O|$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'|OO', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'|O$O', ['', 'a']) + + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((1,), {}, b'OO', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((1,), {}, b'O$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {}, b'O|O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {}, b'O|$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {}, b'|OO', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {}, b'|O$O', ['', 'a']) + + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {}, b'OO', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {}, b'O$O', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {}, b'O|O', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {}, b'O|$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((), {}, b'|OO', ['', 'a']) + _testcapi.parse_tuple_and_keywords((), {}, b'|O$O', ['', 'a']) + + _testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'OO', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'O$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'O|O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'O|$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'|OO', ['', 'a']) + _testcapi.parse_tuple_and_keywords((1,), {'a': 2}, b'|O$O', ['', 'a']) + + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {'a': 2}, b'OO', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {'a': 2}, b'O$O', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {'a': 2}, b'O|O', ['', 'a']) + with self.assertRaises(TypeError): + _testcapi.parse_tuple_and_keywords((), {'a': 2}, b'O|$O', ['', 'a']) + _testcapi.parse_tuple_and_keywords((), {'a': 2}, b'|OO', ['', 'a']) + _testcapi.parse_tuple_and_keywords((), {'a': 2}, b'|O$O', ['', 'a']) + + with self.assertRaises(SystemError): + _testcapi.parse_tuple_and_keywords((1, 2), {}, b'|$OO', ['', 'a']) + + def test_positional_only(self): + parse = _testcapi.parse_tuple_and_keywords + + parse((1, 2, 3), {}, b'OOO', ['', '', 'a']) + parse((1, 2), {'a': 3}, b'OOO', ['', '', 'a']) + with self.assertRaisesRegex(TypeError, + 'Function takes at least 2 positional arguments \(1 given\)'): + parse((1,), {'a': 3}, b'OOO', ['', '', 'a']) + parse((1,), {}, b'O|OO', ['', '', 'a']) + with self.assertRaisesRegex(TypeError, + 'Function takes at least 1 positional arguments \(0 given\)'): + parse((), {}, b'O|OO', ['', '', 'a']) + parse((1, 2), {'a': 3}, b'OO$O', ['', '', 'a']) + with self.assertRaisesRegex(TypeError, + 'Function takes exactly 2 positional arguments \(1 given\)'): + parse((1,), {'a': 3}, b'OO$O', ['', '', 'a']) + parse((1,), {}, b'O|O$O', ['', '', 'a']) + with self.assertRaisesRegex(TypeError, + 'Function takes at least 1 positional arguments \(0 given\)'): + parse((), {}, b'O|O$O', ['', '', 'a']) + with self.assertRaisesRegex(SystemError, 'Empty keyword after \$'): + parse((1,), {}, b'O|$OO', ['', '', 'a']) + with self.assertRaisesRegex(SystemError, 'Empty keyword'): + parse((1,), {}, b'O|OO', ['', 'a', '']) + + @unittest.skipUnless(threading, 'Threading required for this test.') class TestThreadState(unittest.TestCase): diff -r 3084914245d2 Lib/test/test_getargs2.py --- a/Lib/test/test_getargs2.py Mon Feb 08 20:34:49 2016 -0800 +++ b/Lib/test/test_getargs2.py Tue Feb 09 15:51:57 2016 +0200 @@ -470,6 +470,39 @@ class KeywordOnly_TestCase(unittest.Test getargs_keyword_only(1, 2, **{'\uDC80': 10}) +class PositionalOnlyAndKeywords_TestCase(unittest.TestCase): + from _testcapi import getargs_positional_only_and_keywords as getargs + + def test_positional_args(self): + # using all possible positional args + self.assertEqual(self.getargs(1, 2, 3), (1, 2, 3)) + + def test_mixed_args(self): + # positional and keyword args + self.assertEqual(self.getargs(1, 2, keyword=3), (1, 2, 3)) + + def test_optional_args(self): + # missing optional args + self.assertEqual(self.getargs(1, 2), (1, 2, -1)) + self.assertEqual(self.getargs(1, keyword=3), (1, -1, 3)) + + def test_required_args(self): + self.assertEqual(self.getargs(1), (1, -1, -1)) + # required positional arg missing + with self.assertRaisesRegex(TypeError, + "Function takes at least 1 positional arguments \(0 given\)"): + self.getargs() + + with self.assertRaisesRegex(TypeError, + "Function takes at least 1 positional arguments \(0 given\)"): + self.getargs(keyword=3) + + def test_empty_keyword(self): + with self.assertRaisesRegex(TypeError, + "'' is an invalid keyword argument for this function"): + self.getargs(1, 2, **{'': 666}) + + class Bytes_TestCase(unittest.TestCase): def test_c(self): from _testcapi import getargs_c diff -r 3084914245d2 Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c Mon Feb 08 20:34:49 2016 -0800 +++ b/Modules/_testcapimodule.c Tue Feb 09 15:51:57 2016 +0200 @@ -914,6 +914,21 @@ getargs_keyword_only(PyObject *self, PyO return Py_BuildValue("iii", required, optional, keyword_only); } +/* test PyArg_ParseTupleAndKeywords positional-only arguments */ +static PyObject * +getargs_positional_only_and_keywords(PyObject *self, PyObject *args, PyObject *kwargs) +{ + static char *keywords[] = {"", "", "keyword", NULL}; + int required = -1; + int optional = -1; + int keyword = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", keywords, + &required, &optional, &keyword)) + return NULL; + return Py_BuildValue("iii", required, optional, keyword); +} + /* Functions to call PyArg_ParseTuple with integer format codes, and return the result. */ @@ -3658,6 +3673,9 @@ static PyMethodDef TestMethods[] = { METH_VARARGS|METH_KEYWORDS}, {"getargs_keyword_only", (PyCFunction)getargs_keyword_only, METH_VARARGS|METH_KEYWORDS}, + {"getargs_positional_only_and_keywords", + (PyCFunction)getargs_positional_only_and_keywords, + METH_VARARGS|METH_KEYWORDS}, {"getargs_b", getargs_b, METH_VARARGS}, {"getargs_B", getargs_B, METH_VARARGS}, {"getargs_h", getargs_h, METH_VARARGS}, diff -r 3084914245d2 Python/getargs.c --- a/Python/getargs.c Mon Feb 08 20:34:49 2016 -0800 +++ b/Python/getargs.c Tue Feb 09 15:51:57 2016 +0200 @@ -1453,7 +1453,8 @@ vgetargskeywords(PyObject *args, PyObjec const char *fname, *msg, *custom_msg, *keyword; int min = INT_MAX; int max = INT_MAX; - int i, len; + int i, pos, len; + int skip = 0; Py_ssize_t nargs, nkeywords; PyObject *current_arg; freelistentry_t static_entries[STATIC_FREELIST_ENTRIES]; @@ -1482,8 +1483,15 @@ vgetargskeywords(PyObject *args, PyObjec } /* scan kwlist and get greatest possible nbr of args */ - for (len=0; kwlist[len]; len++) - continue; + for (pos = 0; kwlist[pos] && !*kwlist[pos]; pos++) { + } + for (len = pos; kwlist[len]; len++) { + if (!*kwlist[len]) { + PyErr_SetString(PyExc_SystemError, + "Empty keyword"); + return cleanreturn(0, &freelist); + } + } if (len > STATIC_FREELIST_ENTRIES) { freelist.entries = PyMem_NEW(freelistentry_t, len); @@ -1536,6 +1544,14 @@ vgetargskeywords(PyObject *args, PyObjec max = i; format++; + if (max < pos) { + PyErr_SetString(PyExc_SystemError, + "Empty keyword after $"); + return cleanreturn(0, &freelist); + } + if (skip) { + break; + } if (max < nargs) { PyErr_Format(PyExc_TypeError, "Function takes %s %d positional arguments" @@ -1551,48 +1567,59 @@ vgetargskeywords(PyObject *args, PyObjec "format specifiers (%d)", len, i); return cleanreturn(0, &freelist); } - current_arg = NULL; - if (nkeywords) { - current_arg = PyDict_GetItemString(keywords, keyword); - } - if (current_arg) { - --nkeywords; - if (i < nargs) { - /* arg present in tuple and in dict */ - PyErr_Format(PyExc_TypeError, - "Argument given by name ('%s') " - "and position (%d)", - keyword, i+1); - return cleanreturn(0, &freelist); + if (!skip) { + current_arg = NULL; + if (nkeywords && i >= pos) { + current_arg = PyDict_GetItemString(keywords, keyword); + if (!current_arg && PyErr_Occurred()) { + return cleanreturn(0, &freelist); + } + } + if (current_arg) { + --nkeywords; + if (i < nargs) { + /* arg present in tuple and in dict */ + PyErr_Format(PyExc_TypeError, + "Argument given by name ('%s') " + "and position (%d)", + keyword, i+1); + return cleanreturn(0, &freelist); + } + } + else if (i < nargs) + current_arg = PyTuple_GET_ITEM(args, i); + + if (current_arg) { + msg = convertitem(current_arg, &format, p_va, flags, + levels, msgbuf, sizeof(msgbuf), &freelist); + if (msg) { + seterror(i+1, msg, levels, fname, custom_msg); + return cleanreturn(0, &freelist); + } + continue; + } + + if (i < min) { + if (i < pos) { + assert (min == INT_MAX); + assert (max == INT_MAX); + skip = 1; + } + else { + PyErr_Format(PyExc_TypeError, "Required argument " + "'%s' (pos %d) not found", + keyword, i+1); + return cleanreturn(0, &freelist); + } + } + /* current code reports success when all required args + * fulfilled and no keyword args left, with no further + * validation. XXX Maybe skip this in debug build ? + */ + if (!nkeywords && !skip) { + return cleanreturn(1, &freelist); } } - else if (nkeywords && PyErr_Occurred()) - return cleanreturn(0, &freelist); - else if (i < nargs) - current_arg = PyTuple_GET_ITEM(args, i); - - if (current_arg) { - msg = convertitem(current_arg, &format, p_va, flags, - levels, msgbuf, sizeof(msgbuf), &freelist); - if (msg) { - seterror(i+1, msg, levels, fname, custom_msg); - return cleanreturn(0, &freelist); - } - continue; - } - - if (i < min) { - PyErr_Format(PyExc_TypeError, "Required argument " - "'%s' (pos %d) not found", - keyword, i+1); - return cleanreturn(0, &freelist); - } - /* current code reports success when all required args - * fulfilled and no keyword args left, with no further - * validation. XXX Maybe skip this in debug build ? - */ - if (!nkeywords) - return cleanreturn(1, &freelist); /* We are into optional args, skip thru to any remaining * keyword args */ @@ -1604,6 +1631,15 @@ vgetargskeywords(PyObject *args, PyObjec } } + if (skip) { + PyErr_Format(PyExc_TypeError, + "Function takes %s %d positional arguments" + " (%d given)", + (Py_MIN(pos, min) < i) ? "at least" : "exactly", + Py_MIN(pos, min), nargs); + return cleanreturn(0, &freelist); + } + if (!IS_END_OF_FORMAT(*format) && (*format != '|') && (*format != '$')) { PyErr_Format(PyExc_RuntimeError, "more argument specifiers than keyword list entries " @@ -1623,7 +1659,7 @@ vgetargskeywords(PyObject *args, PyObjec return cleanreturn(0, &freelist); } for (i = 0; i < len; i++) { - if (!PyUnicode_CompareWithASCIIString(key, kwlist[i])) { + if (*kwlist[i] && !PyUnicode_CompareWithASCIIString(key, kwlist[i])) { match = 1; break; }