bpo-37540: vectorcall: keyword names must be strings (GH-14682)
authorJeroen Demeyer <J.Demeyer@UGent.be>
Fri, 16 Aug 2019 10:41:27 +0000 (12:41 +0200)
committerMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 16 Aug 2019 10:41:27 +0000 (03:41 -0700)
The fact that keyword names are strings is now part of the vectorcall and `METH_FASTCALL` protocols. The biggest concrete change is that `_PyStack_UnpackDict` now checks that and raises `TypeError` if not.

CC @markshannon @vstinner

https://bugs.python.org/issue37540

Doc/c-api/object.rst
Doc/c-api/structures.rst
Doc/library/dis.rst
Include/cpython/abstract.h
Lib/test/test_extcall.py
Lib/test/test_unpack_ex.py
Misc/NEWS.d/next/C API/2019-07-10-12-27-28.bpo-37540.E8Z773.rst [new file with mode: 0644]
Objects/call.c
Python/ceval.c
Python/getargs.c

index 2cf032821afbe5da9bf718fad3aa014cd66141a9..fd1e9c65aaba97e922a20dcf3c7471604fcb9eb8 100644 (file)
@@ -400,8 +400,8 @@ Object Protocol
    :c:func:`PyVectorcall_NARGS(nargsf) <PyVectorcall_NARGS>`.
 
    *kwnames* can be either NULL (no keyword arguments) or a tuple of keyword
-   names. In the latter case, the values of the keyword arguments are stored
-   in *args* after the positional arguments.
+   names, which must be strings. In the latter case, the values of the keyword
+   arguments are stored in *args* after the positional arguments.
    The number of keyword arguments does not influence *nargsf*.
 
    *kwnames* must contain only objects of type ``str`` (not a subclass),
index 5184ad511cd9b1e1952ea88a1ddac4beaf3f5d09..d4e65afef14d53546bf57ce18b19448aebe832a3 100644 (file)
@@ -204,6 +204,7 @@ also keyword arguments.  So there are a total of 6 calling conventions:
    Keyword arguments are passed the same way as in the vectorcall protocol:
    there is an additional fourth :c:type:`PyObject\*` parameter
    which is a tuple representing the names of the keyword arguments
+   (which are guaranteed to be strings)
    or possibly *NULL* if there are no keywords.  The values of the keyword
    arguments are stored in the *args* array, after the positional arguments.
 
index 39a3e130afd3e0fcab3466b88473779f99246f3e..4a20245e3032b99ef13eb69e03de61bab9d97a7f 100644 (file)
@@ -1142,8 +1142,10 @@ All of the following opcodes use their arguments.
 
    Calls a callable object with positional (if any) and keyword arguments.
    *argc* indicates the total number of positional and keyword arguments.
-   The top element on the stack contains a tuple of keyword argument names.
-   Below that are keyword arguments in the order corresponding to the tuple.
+   The top element on the stack contains a tuple with the names of the
+   keyword arguments, which must be strings.
+   Below that are the values for the keyword arguments,
+   in the order corresponding to the tuple.
    Below that are positional arguments, with the right-most parameter on
    top.  Below the arguments is a callable object to call.
    ``CALL_FUNCTION_KW`` pops all arguments and the callable object off the stack,
index d57aa54bc4661205d3392ef15b3503633e9d8272..62a113fc00e202b4cf2488b6e1e2e16b2262464d 100644 (file)
@@ -88,8 +88,7 @@ _PyVectorcall_Function(PyObject *callable)
    of keyword arguments does not change nargsf). kwnames can also be NULL if
    there are no keyword arguments.
 
-   keywords must only contains str strings (no subclass), and all keys must
-   be unique.
+   keywords must only contain strings and all keys must be unique.
 
    Return the result on success. Raise an exception and return NULL on
    error. */
index 3cac3bda4253b47b1bdbf302864195a37c93bb12..d9dcb709f7541581a2d15c99fd097b2b8502d414 100644 (file)
@@ -237,7 +237,7 @@ What about willful misconduct?
     >>> f(**{1:2})
     Traceback (most recent call last):
       ...
-    TypeError: f() keywords must be strings
+    TypeError: keywords must be strings
 
     >>> h(**{'e': 2})
     Traceback (most recent call last):
index 45cf051f1ec1a9da8c673f398e1b841170657cb7..87fea593c0201bd76e1b49d4437e2bb5197c5771 100644 (file)
@@ -256,7 +256,7 @@ Overridden parameters
     >>> f(**{1: 3}, **{1: 5})
     Traceback (most recent call last):
       ...
-    TypeError: f() keywords must be strings
+    TypeError: f() got multiple values for keyword argument '1'
 
 Unpacking non-sequence
 
diff --git a/Misc/NEWS.d/next/C API/2019-07-10-12-27-28.bpo-37540.E8Z773.rst b/Misc/NEWS.d/next/C API/2019-07-10-12-27-28.bpo-37540.E8Z773.rst
new file mode 100644 (file)
index 0000000..1a09c7e
--- /dev/null
@@ -0,0 +1,2 @@
+The vectorcall protocol now requires that the caller passes only strings as
+keyword names.
index 7d917891bc0cd9f08ffb00a9055450c7ba35b74d..8a60b3e58e30356deea50b3f737f70f63abe7dfb 100644 (file)
@@ -322,8 +322,7 @@ _PyFunction_Vectorcall(PyObject *func, PyObject* const* stack,
     assert(nargs >= 0);
     assert(kwnames == NULL || PyTuple_CheckExact(kwnames));
     assert((nargs == 0 && nkwargs == 0) || stack != NULL);
-    /* kwnames must only contains str strings, no subclass, and all keys must
-       be unique */
+    /* kwnames must only contain strings and all keys must be unique */
 
     if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
         (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
@@ -943,12 +942,12 @@ _PyStack_AsDict(PyObject *const *values, PyObject *kwnames)
    vector; return NULL with exception set on error. Return the keyword names
    tuple in *p_kwnames.
 
-   The newly allocated argument vector supports PY_VECTORCALL_ARGUMENTS_OFFSET.
+   This also checks that all keyword names are strings. If not, a TypeError is
+   raised.
 
-   When done, you must call _PyStack_UnpackDict_Free(stack, nargs, kwnames)
+   The newly allocated argument vector supports PY_VECTORCALL_ARGUMENTS_OFFSET.
 
-   The type of keyword keys is not checked, these checks should be done
-   later (ex: _PyArg_ParseStackAndKeywords). */
+   When done, you must call _PyStack_UnpackDict_Free(stack, nargs, kwnames) */
 static PyObject *const *
 _PyStack_UnpackDict(PyObject *const *args, Py_ssize_t nargs, PyObject *kwargs,
                     PyObject **p_kwnames)
@@ -994,7 +993,9 @@ _PyStack_UnpackDict(PyObject *const *args, Py_ssize_t nargs, PyObject *kwargs,
        called in the performance critical hot code. */
     Py_ssize_t pos = 0, i = 0;
     PyObject *key, *value;
+    unsigned long keys_are_strings = Py_TPFLAGS_UNICODE_SUBCLASS;
     while (PyDict_Next(kwargs, &pos, &key, &value)) {
+        keys_are_strings &= Py_TYPE(key)->tp_flags;
         Py_INCREF(key);
         Py_INCREF(value);
         PyTuple_SET_ITEM(kwnames, i, key);
@@ -1002,6 +1003,18 @@ _PyStack_UnpackDict(PyObject *const *args, Py_ssize_t nargs, PyObject *kwargs,
         i++;
     }
 
+    /* keys_are_strings has the value Py_TPFLAGS_UNICODE_SUBCLASS if that
+     * flag is set for all keys. Otherwise, keys_are_strings equals 0.
+     * We do this check once at the end instead of inside the loop above
+     * because it simplifies the deallocation in the failing case.
+     * It happens to also make the loop above slightly more efficient. */
+    if (!keys_are_strings) {
+        PyErr_SetString(PyExc_TypeError,
+                        "keywords must be strings");
+        _PyStack_UnpackDict_Free(stack, nargs, kwnames);
+        return NULL;
+    }
+
     *p_kwnames = kwnames;
     return stack;
 }
index 7c7359166dad91f20a82c79a209c2266a1a705ac..ee03350031d91ed57c51ec6bb8a13e59debb965d 100644 (file)
@@ -3504,7 +3504,9 @@ main_loop:
             PyObject **sp, *res, *names;
 
             names = POP();
-            assert(PyTuple_CheckExact(names) && PyTuple_GET_SIZE(names) <= oparg);
+            assert(PyTuple_Check(names));
+            assert(PyTuple_GET_SIZE(names) <= oparg);
+            /* We assume without checking that names contains only strings */
             sp = stack_pointer;
             res = call_function(tstate, &sp, oparg, names);
             stack_pointer = sp;
@@ -5372,20 +5374,12 @@ format_kwargs_error(PyThreadState *tstate, PyObject *func, PyObject *kwargs)
         _PyErr_Fetch(tstate, &exc, &val, &tb);
         if (val && PyTuple_Check(val) && PyTuple_GET_SIZE(val) == 1) {
             PyObject *key = PyTuple_GET_ITEM(val, 0);
-            if (!PyUnicode_Check(key)) {
-                _PyErr_Format(tstate, PyExc_TypeError,
-                              "%.200s%.200s keywords must be strings",
-                              PyEval_GetFuncName(func),
-                              PyEval_GetFuncDesc(func));
-            }
-            else {
-                _PyErr_Format(tstate, PyExc_TypeError,
-                              "%.200s%.200s got multiple "
-                              "values for keyword argument '%U'",
-                              PyEval_GetFuncName(func),
-                              PyEval_GetFuncDesc(func),
-                              key);
-            }
+            _PyErr_Format(tstate, PyExc_TypeError,
+                          "%.200s%.200s got multiple "
+                          "values for keyword argument '%S'",
+                          PyEval_GetFuncName(func),
+                          PyEval_GetFuncDesc(func),
+                          key);
             Py_XDECREF(exc);
             Py_XDECREF(val);
             Py_XDECREF(tb);
index 59f0fdabb74a656f0e0ea1e98ea94a82c6c85691..cdc16d4730b54677ca81afea08f53fcad2201907 100644 (file)
@@ -2043,11 +2043,7 @@ find_keyword(PyObject *kwnames, PyObject *const *kwstack, PyObject *key)
         if (kwname == key) {
             return kwstack[i];
         }
-        if (!PyUnicode_Check(kwname)) {
-            /* ignore non-string keyword keys:
-               an error will be raised below */
-            continue;
-        }
+        assert(PyUnicode_Check(kwname));
         if (_PyUnicode_EQ(kwname, key)) {
             return kwstack[i];
         }
@@ -2275,16 +2271,11 @@ vgetargskeywordsfast_impl(PyObject *const *args, Py_ssize_t nargs,
                 j++;
             }
 
-            if (!PyUnicode_Check(keyword)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "keywords must be strings");
-                return cleanreturn(0, &freelist);
-            }
             match = PySequence_Contains(kwtuple, keyword);
             if (match <= 0) {
                 if (!match) {
                     PyErr_Format(PyExc_TypeError,
-                                 "'%U' is an invalid keyword "
+                                 "'%S' is an invalid keyword "
                                  "argument for %.200s%s",
                                  keyword,
                                  (parser->fname == NULL) ? "this function" : parser->fname,
@@ -2505,16 +2496,11 @@ _PyArg_UnpackKeywords(PyObject *const *args, Py_ssize_t nargs,
                 j++;
             }
 
-            if (!PyUnicode_Check(keyword)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "keywords must be strings");
-                return NULL;
-            }
             match = PySequence_Contains(kwtuple, keyword);
             if (match <= 0) {
                 if (!match) {
                     PyErr_Format(PyExc_TypeError,
-                                 "'%U' is an invalid keyword "
+                                 "'%S' is an invalid keyword "
                                  "argument for %.200s%s",
                                  keyword,
                                  (parser->fname == NULL) ? "this function" : parser->fname,