Skip to content
39 changes: 39 additions & 0 deletions Lib/test/test_extcall.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,45 @@
...
TypeError: f() missing 5 required keyword-only arguments: 'a', 'b', 'c', 'd', and 'e'

Test that functions are referred to by their __qualname__s:

>>> class A:
... def method(self, x, y):
... pass
... @staticmethod
... def static_method():
... pass
... @staticmethod
... def positional_only(arg, /):
... pass

>>> a_object = A()

>>> a_object.method("x")
Traceback (most recent call last):
...
TypeError: A.method() missing 1 required positional argument: 'y'

>>> A.static_method("oops it's an argument")
Traceback (most recent call last):
...
TypeError: A.static_method() takes 0 positional arguments but 1 was given

>>> A.positional_only(arg="x")
Traceback (most recent call last):
...
TypeError: A.positional_only() got some positional-only arguments passed as keyword arguments: 'arg'

>>> a_object.method(bad='x')
Traceback (most recent call last):
...
TypeError: A.method() got an unexpected keyword argument 'bad'

>>> a_object.method("x", "y", x="oops")
Traceback (most recent call last):
...
TypeError: A.method() got multiple values for argument 'x'

"""

import sys
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_keywordonlyarg.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def f(a, b=None, *, c=None):
pass
with self.assertRaises(TypeError) as exc:
f(1, 2, 3)
expected = "f() takes from 1 to 2 positional arguments but 3 were given"
expected = (f"{f.__qualname__}() takes from 1 to 2 "
"positional arguments but 3 were given")
self.assertEqual(str(exc.exception), expected)

def testSyntaxErrorForFunctionCall(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Certain error messages about too few or too many arguments now refer to a function by its :term:`qualified name`.
31 changes: 16 additions & 15 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -3875,7 +3875,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)

static void
format_missing(PyThreadState *tstate, const char *kind,
PyCodeObject *co, PyObject *names)
PyCodeObject *co, PyObject *names, PyObject *qualname)
{
int err;
Py_ssize_t len = PyList_GET_SIZE(names);
Expand Down Expand Up @@ -3928,7 +3928,7 @@ format_missing(PyThreadState *tstate, const char *kind,
return;
_PyErr_Format(tstate, PyExc_TypeError,
"%U() missing %i required %s argument%s: %U",
co->co_name,
qualname,
len,
kind,
len == 1 ? "" : "s",
Expand All @@ -3939,7 +3939,7 @@ format_missing(PyThreadState *tstate, const char *kind,
static void
missing_arguments(PyThreadState *tstate, PyCodeObject *co,
Py_ssize_t missing, Py_ssize_t defcount,
PyObject **fastlocals)
PyObject **fastlocals, PyObject *qualname)
{
Py_ssize_t i, j = 0;
Py_ssize_t start, end;
Expand Down Expand Up @@ -3971,14 +3971,14 @@ missing_arguments(PyThreadState *tstate, PyCodeObject *co,
}
}
assert(j == missing);
format_missing(tstate, kind, co, missing_names);
format_missing(tstate, kind, co, missing_names, qualname);
Py_DECREF(missing_names);
}

static void
too_many_positional(PyThreadState *tstate, PyCodeObject *co,
Py_ssize_t given, Py_ssize_t defcount,
PyObject **fastlocals)
PyObject **fastlocals, PyObject *qualname)
{
int plural;
Py_ssize_t kwonly_given = 0;
Expand Down Expand Up @@ -4022,7 +4022,7 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co,
}
_PyErr_Format(tstate, PyExc_TypeError,
"%U() takes %U positional argument%s but %zd%U %s given",
co->co_name,
qualname,
sig,
plural ? "s" : "",
given,
Expand All @@ -4034,7 +4034,8 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co,

static int
positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co,
Py_ssize_t kwcount, PyObject* const* kwnames)
Py_ssize_t kwcount, PyObject* const* kwnames,
PyObject *qualname)
{
int posonly_conflicts = 0;
PyObject* posonly_names = PyList_New(0);
Expand Down Expand Up @@ -4079,7 +4080,7 @@ positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co,
_PyErr_Format(tstate, PyExc_TypeError,
"%U() got some positional-only arguments passed"
" as keyword arguments: '%U'",
co->co_name, error_names);
qualname, error_names);
Py_DECREF(error_names);
goto fail;
}
Expand Down Expand Up @@ -4180,7 +4181,7 @@ _PyEval_EvalCode(PyThreadState *tstate,
if (keyword == NULL || !PyUnicode_Check(keyword)) {
_PyErr_Format(tstate, PyExc_TypeError,
"%U() keywords must be strings",
co->co_name);
qualname);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any suggestions for how to test this from Python? I couldn't figure out how without getting a SyntaxError.

Copy link
Contributor

@SylvainDe SylvainDe May 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the ** should do the trick.
For example:

>>> func = lambda *args, **kwargs: None >>> kwargs = {None: None} >>> func(kwargs) >>> func(*kwargs) >>> func(**kwargs) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: <lambda>() keywords must be strings 
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it looks like this behavior changed a bit in the newest releases so that that code isn't actually reached:

PS > py -3.7 -c "f = lambda x: None; f(**{1:1})" Traceback (most recent call last): File "<string>", line 1, in <module> TypeError: <lambda>() keywords must be strings PS > py -3.8 -c "f = lambda x: None; f(**{1:1})" Traceback (most recent call last): File "<string>", line 1, in <module> TypeError: <lambda>() keywords must be strings PS > py -3.9 -c "f = lambda x: None; f(**{1:1})" Traceback (most recent call last): File "<string>", line 1, in <module> TypeError: keywords must be strings 
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see... Probably related to 0567786 .

goto fail;
}

Expand Down Expand Up @@ -4211,14 +4212,14 @@ _PyEval_EvalCode(PyThreadState *tstate,

if (co->co_posonlyargcount
&& positional_only_passed_as_keyword(tstate, co,
kwcount, kwnames))
kwcount, kwnames, qualname))
{
goto fail;
}

_PyErr_Format(tstate, PyExc_TypeError,
"%U() got an unexpected keyword argument '%S'",
co->co_name, keyword);
qualname, keyword);
goto fail;
}

Expand All @@ -4231,7 +4232,7 @@ _PyEval_EvalCode(PyThreadState *tstate,
if (GETLOCAL(j) != NULL) {
_PyErr_Format(tstate, PyExc_TypeError,
"%U() got multiple values for argument '%S'",
co->co_name, keyword);
qualname, keyword);
goto fail;
}
Py_INCREF(value);
Expand All @@ -4240,7 +4241,7 @@ _PyEval_EvalCode(PyThreadState *tstate,

/* Check the number of positional arguments */
if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
too_many_positional(tstate, co, argcount, defcount, fastlocals);
too_many_positional(tstate, co, argcount, defcount, fastlocals, qualname);
goto fail;
}

Expand All @@ -4254,7 +4255,7 @@ _PyEval_EvalCode(PyThreadState *tstate,
}
}
if (missing) {
missing_arguments(tstate, co, missing, defcount, fastlocals);
missing_arguments(tstate, co, missing, defcount, fastlocals, qualname);
goto fail;
}
if (n > m)
Expand Down Expand Up @@ -4292,7 +4293,7 @@ _PyEval_EvalCode(PyThreadState *tstate,
missing++;
}
if (missing) {
missing_arguments(tstate, co, missing, -1, fastlocals);
missing_arguments(tstate, co, missing, -1, fastlocals, qualname);
goto fail;
}
}
Expand Down