[Python-checkins] cpython: give the names of missing positional or keyword-only arguments (closes #12356)

benjamin.peterson python-checkins at python.org
Fri Jun 24 16:37:38 CEST 2011


http://hg.python.org/cpython/rev/52744a5a9260
changeset:   70940:52744a5a9260
user:        Benjamin Peterson <benjamin at python.org>
date:        Fri Jun 24 09:37:26 2011 -0500
summary:
  give the names of missing positional or keyword-only arguments (closes #12356)

files:
  Lib/inspect.py           |   41 +++++--
  Lib/test/test_extcall.py |  113 +++++++++-----------
  Misc/NEWS                |    4 +
  Python/ceval.c           |  141 ++++++++++++++++++++++----
  4 files changed, 202 insertions(+), 97 deletions(-)


diff --git a/Lib/inspect.py b/Lib/inspect.py
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -918,10 +918,24 @@
         specs.append(formatvarkw(varkw) + formatvalue(locals[varkw]))
     return '(' + ', '.join(specs) + ')'
 
-def _positional_error(f_name, args, kwonly, varargs, defcount, given, values):
+def _missing_arguments(f_name, argnames, pos, values):
+    names = [repr(name) for name in argnames if name not in values]
+    missing = len(names)
+    if missing == 1:
+        s = names[0]
+    elif missing == 2:
+        s = "{} and {}".format(*names)
+    else:
+        tail = ", {} and {}".format(names[-2:])
+        del names[-2:]
+        s = ", ".join(names) + tail
+    raise TypeError("%s() missing %i required %s argument%s: %s" %
+                    (f_name, missing,
+                      "positional" if pos else "keyword-only",
+                      "" if missing == 1 else "s", s))
+
+def _too_many(f_name, args, kwonly, varargs, defcount, given, values):
     atleast = len(args) - defcount
-    if given is None:
-        given = len([arg for arg in args if arg in values])
     kwonly_given = len([arg for arg in kwonly if arg in values])
     if varargs:
         plural = atleast != 1
@@ -980,22 +994,25 @@
                             (f_name, kw))
         arg2value[kw] = value
     if num_pos > num_args and not varargs:
-        _positional_error(f_name, args, kwonlyargs, varargs, num_defaults,
-                          num_pos, arg2value)
+        _too_many(f_name, args, kwonlyargs, varargs, num_defaults,
+                   num_pos, arg2value)
     if num_pos < num_args:
-        for arg in args[:num_args - num_defaults]:
+        req = args[:num_args - num_defaults]
+        for arg in req:
             if arg not in arg2value:
-                _positional_error(f_name, args, kwonlyargs, varargs,
-                                  num_defaults, None, arg2value)
+                _missing_arguments(f_name, req, True, arg2value)
         for i, arg in enumerate(args[num_args - num_defaults:]):
             if arg not in arg2value:
                 arg2value[arg] = defaults[i]
+    missing = 0
     for kwarg in kwonlyargs:
         if kwarg not in arg2value:
-            if kwarg not in kwonlydefaults:
-                raise TypeError("%s() requires keyword-only argument %r" %
-                                (f_name, kwarg))
-            arg2value[kwarg] = kwonlydefaults[kwarg]
+            if kwarg in kwonlydefaults:
+                arg2value[kwarg] = kwonlydefaults[kwarg]
+            else:
+                missing += 1
+    if missing:
+        _missing_arguments(f_name, kwonlyargs, False, arg2value)
     return arg2value
 
 # -------------------------------------------------- stack frame extraction
diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py
--- a/Lib/test/test_extcall.py
+++ b/Lib/test/test_extcall.py
@@ -66,17 +66,17 @@
     >>> g()
     Traceback (most recent call last):
       ...
-    TypeError: g() takes at least 1 positional argument but 0 were given
+    TypeError: g() missing 1 required positional argument: 'x'
 
     >>> g(*())
     Traceback (most recent call last):
       ...
-    TypeError: g() takes at least 1 positional argument but 0 were given
+    TypeError: g() missing 1 required positional argument: 'x'
 
     >>> g(*(), **{})
     Traceback (most recent call last):
       ...
-    TypeError: g() takes at least 1 positional argument but 0 were given
+    TypeError: g() missing 1 required positional argument: 'x'
 
     >>> g(1)
     1 () {}
@@ -263,91 +263,80 @@
     >>> f(**x)
     1 2
 
-Some additional tests about positional argument errors:
+Too many arguments:
 
-    >>> def f(a, b):
-    ...    pass
-    >>> f(b=1)
+    >>> def f(): pass
+    >>> f(1)
     Traceback (most recent call last):
       ...
-    TypeError: f() takes 2 positional arguments but 1 was given
-
-    >>> def f(a):
-    ...    pass
-    >>> f(6, a=4, *(1, 2, 3))
+    TypeError: f() takes 0 positional arguments but 1 was given
+    >>> def f(a): pass
+    >>> f(1, 2)
     Traceback (most recent call last):
       ...
-    TypeError: f() got multiple values for argument 'a'
-    >>> def f(a, *, kw):
-    ...    pass
-    >>> f(6, 4, kw=4)
+    TypeError: f() takes 1 positional argument but 2 were given
+    >>> def f(a, b=1): pass
+    >>> f(1, 2, 3)
     Traceback (most recent call last):
       ...
-    TypeError: f() takes 1 positional argument but 2 positional arguments (and 1 keyword-only argument) were given
+    TypeError: f() takes from 1 to 2 positional arguments but 3 were given
+    >>> def f(*, kw): pass
+    >>> f(1, kw=3)
+    Traceback (most recent call last):
+      ...
+    TypeError: f() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
+    >>> def f(*, kw, b): pass
+    >>> f(1, 2, 3, b=3, kw=3)
+    Traceback (most recent call last):
+      ...
+    TypeError: f() takes 0 positional arguments but 3 positional arguments (and 2 keyword-only arguments) were given
+    >>> def f(a, b=2, *, kw): pass
+    >>> f(2, 3, 4, kw=4)
+    Traceback (most recent call last):
+      ...
+    TypeError: f() takes from 1 to 2 positional arguments but 3 positional arguments (and 1 keyword-only argument) were given
 
-    >>> def f(a):
-    ...    pass
+Too few and missing arguments:
+
+    >>> def f(a): pass
     >>> f()
     Traceback (most recent call last):
       ...
-    TypeError: f() takes 1 positional argument but 0 were given
-
-    >>> def f(a, b):
-    ...    pass
-    >>> f(1)
-    Traceback (most recent call last):
-      ...
-    TypeError: f() takes 2 positional arguments but 1 was given
-
-    >>> def f(a, *b):
-    ...    pass
+    TypeError: f() missing 1 required positional argument: 'a'
+    >>> def f(a, b): pass
     >>> f()
     Traceback (most recent call last):
       ...
-    TypeError: f() takes at least 1 positional argument but 0 were given
-
-    >>> def f(a, *, kw=4):
-    ...    pass
-    >>> f(kw=4)
-    Traceback (most recent call last):
-      ...
-    TypeError: f() takes 1 positional argument but 0 positional arguments (and 1 keyword-only argument) were given
-
-    >>> def f(a, b=2):
-    ...    pass
+    TypeError: f() missing 2 required positional arguments: 'a' and 'b'
+    >>> def f(a, b, c): pass
     >>> f()
     Traceback (most recent call last):
       ...
-    TypeError: f() takes from 1 to 2 positional arguments but 0 were given
-
-    >>> def f(a, *b):
-    ...    pass
+    TypeError: f() missing 3 required positional arguments: 'a', 'b', and 'c'
+    >>> def f(a, b, c, d, e): pass
     >>> f()
     Traceback (most recent call last):
       ...
-    TypeError: f() takes at least 1 positional argument but 0 were given
-
-    >>> def f(*, kw):
-    ...    pass
-    >>> f(3, kw=4)
+    TypeError: f() missing 5 required positional arguments: 'a', 'b', 'c', 'd', and 'e'
+    >>> def f(a, b=4, c=5, d=5): pass
+    >>> f(c=12, b=9)
     Traceback (most recent call last):
       ...
-    TypeError: f() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
+    TypeError: f() missing 1 required positional argument: 'a'
 
-    >>> def f(a, c=3, *b, kw):
-    ...    pass
+Same with keyword only args:
+
+    >>> def f(*, w): pass
     >>> f()
     Traceback (most recent call last):
-     ...
-    TypeError: f() takes at least 1 positional argument but 0 were given
-    >>> f(kw=3)
+      ...
+    TypeError: f() missing 1 required keyword-only argument: 'w'
+    >>> def f(*, a, b, c, d, e): pass
+    >>> f()
     Traceback (most recent call last):
-     ...
-    TypeError: f() takes at least 1 positional argument but 0 positional arguments (and 1 keyword-only argument) were given
-    >>> f(kw=3, c=4)
-    Traceback (most recent call last):
-     ...
-    TypeError: f() takes at least 1 positional argument but 1 positional argument (and 1 keyword-only argument) were given
+      ...
+    TypeError: f() missing 5 required keyword-only arguments: 'a', 'b', 'c', 'd', and 'e'
+
 """
 
 import sys
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,10 @@
 Core and Builtins
 -----------------
 
+- Issue #12356: When required positional or keyword-only arguments are not
+  given, produce a informative error message which includes the name(s) of the
+  missing arguments.
+
 - Issue #12370: Fix super with not arguments when __class__ is overriden in the
   class body.
 
diff --git a/Python/ceval.c b/Python/ceval.c
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -3046,28 +3046,118 @@
 }
 
 static void
-positional_argument_error(PyCodeObject *co, int given, int defcount, PyObject **fastlocals)
+format_missing(const char *kind, PyCodeObject *co, PyObject *names)
+{
+    int err;
+    Py_ssize_t len = PyList_GET_SIZE(names);
+    PyObject *name_str, *comma, *tail, *tmp;
+
+    assert(PyList_CheckExact(names));
+    assert(len >= 1);
+    /* Deal with the joys of natural language. */
+    switch (len) {
+    case 1:
+        name_str = PyList_GET_ITEM(names, 0);
+        Py_INCREF(name_str);
+        break;
+    case 2:
+        name_str = PyUnicode_FromFormat("%U and %U",
+                                        PyList_GET_ITEM(names, len - 2),
+                                        PyList_GET_ITEM(names, len - 1));
+        break;
+    default:
+        tail = PyUnicode_FromFormat(", %U, and %U",
+                                    PyList_GET_ITEM(names, len - 2),
+                                    PyList_GET_ITEM(names, len - 1));
+        /* Chop off the last two objects in the list. This shouldn't actually
+           fail, but we can't be too careful. */
+        err = PyList_SetSlice(names, len - 2, len, NULL);
+        if (err == -1) {
+            Py_DECREF(tail);
+            return;
+        }
+        /* Stitch everything up into a nice comma-separated list. */
+        comma = PyUnicode_FromString(", ");
+        if (comma == NULL) {
+            Py_DECREF(tail);
+            return;
+        }
+        tmp = PyUnicode_Join(comma, names);
+        Py_DECREF(comma);
+        if (tmp == NULL) {
+            Py_DECREF(tail);
+            return;
+        }
+        name_str = PyUnicode_Concat(tmp, tail);
+        Py_DECREF(tmp);
+        Py_DECREF(tail);
+        break;
+    }
+    if (name_str == NULL)
+        return;
+    PyErr_Format(PyExc_TypeError,
+                 "%U() missing %i required %s argument%s: %U",
+                 co->co_name,
+                 len,
+                 kind,
+                 len == 1 ? "" : "s",
+                 name_str);
+    Py_DECREF(name_str);
+}
+
+static void
+missing_arguments(PyCodeObject *co, int missing, int defcount,
+                  PyObject **fastlocals)
+{
+    int i, j = 0;
+    int start, end;
+    int positional = defcount != -1;
+    const char *kind = positional ? "positional" : "keyword-only";
+    PyObject *missing_names;
+
+    /* Compute the names of the arguments that are missing. */
+    missing_names = PyList_New(missing);
+    if (missing_names == NULL)
+        return;
+    if (positional) {
+        start = 0;
+        end = co->co_argcount - defcount;
+    }
+    else {
+        start = co->co_argcount;
+        end = start + co->co_kwonlyargcount;
+    }
+    for (i = start; i < end; i++) {
+        if (GETLOCAL(i) == NULL) {
+            PyObject *raw = PyTuple_GET_ITEM(co->co_varnames, i);
+            PyObject *name = PyObject_Repr(raw);
+            if (name == NULL) {
+                Py_DECREF(missing_names);
+                return;
+            }
+            PyList_SET_ITEM(missing_names, j++, name);
+        }
+    }
+    assert(j == missing);
+    format_missing(kind, co, missing_names);
+    Py_DECREF(missing_names);
+}
+
+static void
+too_many_positional(PyCodeObject *co, int given, int defcount, PyObject **fastlocals)
 {
     int plural;
     int kwonly_given = 0;
-    int atleast = co->co_argcount - defcount;
     int i;
     PyObject *sig, *kwonly_sig;
 
-    if (given == -1) {
-        given = 0;
-        for (i = 0; i < co->co_argcount; i++)
-            if (GETLOCAL(i))
-                given++;
-    }
+    assert((co->co_flags & CO_VARARGS) == 0);
+    /* Count missing keyword-only args. */
     for (i = co->co_argcount; i < co->co_argcount + co->co_kwonlyargcount; i++)
-        if (GETLOCAL(i))
+        if (GETLOCAL(i) != NULL)
             kwonly_given++;
-    if (co->co_flags & CO_VARARGS) {
-        plural = atleast != 1;
-        sig = PyUnicode_FromFormat("at least %d", atleast);
-    }
-    else if (defcount) {
+    if (defcount) {
+        int atleast = co->co_argcount - defcount;
         plural = 1;
         sig = PyUnicode_FromFormat("from %d to %d", atleast, co->co_argcount);
     }
@@ -3089,6 +3179,7 @@
     else {
         /* This will not fail. */
         kwonly_sig = PyUnicode_FromString("");
+        assert(kwonly_sig != NULL);
     }
     PyErr_Format(PyExc_TypeError,
                  "%U() takes %U positional argument%s but %d%U %s given",
@@ -3217,16 +3308,18 @@
         SETLOCAL(j, value);
     }
     if (argcount > co->co_argcount && !(co->co_flags & CO_VARARGS)) {
-        positional_argument_error(co, argcount, defcount, fastlocals);
+        too_many_positional(co, argcount, defcount, fastlocals);
         goto fail;
     }
     if (argcount < co->co_argcount) {
         int m = co->co_argcount - defcount;
-        for (i = argcount; i < m; i++) {
-            if (GETLOCAL(i) == NULL) {
-                positional_argument_error(co, -1, defcount, fastlocals);
-                goto fail;
-            }
+        int missing = 0;
+        for (i = argcount; i < m; i++)
+            if (GETLOCAL(i) == NULL)
+                missing++;
+        if (missing) {
+            missing_arguments(co, missing, defcount, fastlocals);
+            goto fail;
         }
         if (n > m)
             i = n - m;
@@ -3241,6 +3334,7 @@
         }
     }
     if (co->co_kwonlyargcount > 0) {
+        int missing = 0;
         for (i = co->co_argcount; i < total_args; i++) {
             PyObject *name;
             if (GETLOCAL(i) != NULL)
@@ -3254,9 +3348,10 @@
                     continue;
                 }
             }
-            PyErr_Format(PyExc_TypeError,
-                         "%U() requires keyword-only argument '%S'",
-                         co->co_name, name);
+            missing++;
+        }
+        if (missing) {
+            missing_arguments(co, missing, -1, fastlocals);
             goto fail;
         }
     }

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list