From e109c70860ceb4102a84c32d4bda4e5025eaf92a Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Fri, 24 Jun 2011 09:37:26 -0500 Subject: [PATCH] give the names of missing positional or keyword-only arguments (closes #12356) --- Lib/inspect.py | 41 ++++++++---- Lib/test/test_extcall.py | 133 +++++++++++++++++------------------- Misc/NEWS | 4 ++ Python/ceval.c | 141 ++++++++++++++++++++++++++++++++------- 4 files changed, 212 insertions(+), 107 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index aa4c30f0378..80802e41815 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -918,10 +918,24 @@ def formatargvalues(args, varargs, varkw, locals, 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 @@ def getcallargs(func, *positional, **named): (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 index 49d5441345a..6b6c12de2e9 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -66,17 +66,17 @@ Verify clearing of SF bug #733667 >>> 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 @@ the function call setup. See . >>> f(**x) 1 2 -Some additional tests about positional argument errors: +Too many arguments: - >>> def f(a, b): - ... pass - >>> f(b=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)) - Traceback (most recent call last): - ... - TypeError: f() got multiple values for argument 'a' - >>> def f(a, *, kw): - ... pass - >>> f(6, 4, kw=4) - Traceback (most recent call last): - ... - TypeError: f() takes 1 positional argument but 2 positional arguments (and 1 keyword-only argument) were given - - >>> 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 + >>> def f(): pass >>> f(1) Traceback (most recent call last): ... - TypeError: f() takes 2 positional arguments but 1 was given - - >>> def f(a, *b): - ... pass - >>> f() + TypeError: f() takes 0 positional arguments but 1 was given + >>> def f(a): pass + >>> f(1, 2) 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) + 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 0 positional arguments (and 1 keyword-only argument) were given - - >>> def f(a, b=2): - ... 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 - >>> 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() 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, c=3, *b, kw): - ... pass +Too few and missing arguments: + + >>> def f(a): 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 positional argument: 'a' + >>> def f(a, b): 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) + ... + 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 at least 1 positional argument but 1 positional argument (and 1 keyword-only argument) were given + ... + 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() 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() missing 1 required positional argument: 'a' + +Same with keyword only args: + + >>> def f(*, w): pass + >>> f() + Traceback (most recent call last): + ... + 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() missing 5 required keyword-only arguments: 'a', 'b', 'c', 'd', and 'e' + """ import sys diff --git a/Misc/NEWS b/Misc/NEWS index fc1fa8708b3..4e4b47c7c06 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,10 @@ What's New in Python 3.3 Alpha 1? 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 index f1f4c70da18..ac0707046a6 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3046,28 +3046,118 @@ exit_eval_frame: } 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 @@ positional_argument_error(PyCodeObject *co, int given, int defcount, PyObject ** 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 @@ PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals, 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 @@ PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals, } } 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 @@ PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals, 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; } }