Skip to content
36 changes: 36 additions & 0 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import collections
import itertools
import gc
import re


class FunctionCalls(unittest.TestCase):
Expand Down Expand Up @@ -362,11 +363,46 @@ def class_method(cls):
def static_method():
return "staticmethod"

@staticmethod
def positional_only(arg, /):
return arg


PYTHON_INSTANCE = PythonClass()

NULL_OR_EMPTY = object()


@cpython_only
class TestErrorMessagesUseQualifiedName(unittest.TestCase):

def test_missing_arguments(self):
msg = "PythonClass.method() missing 1 required positional argument: 'arg2'"
with self.assertRaisesRegex(TypeError, "^"+re.escape(msg)+"$"):
PYTHON_INSTANCE.method("arg1")

def test_too_many_positional(self):
msg = "PythonClass.static_method() takes 0 positional arguments but 1 was given"
with self.assertRaisesRegex(TypeError, "^"+re.escape(msg)+"$"):
PythonClass.static_method("oops it's an arg")

def test_positional_only_passed_as_keyword(self):
msg = "PythonClass.positional_only() got some positional-only arguments passed as keyword arguments: 'arg'"
with self.assertRaisesRegex(TypeError, "^"+re.escape(msg)+"$"):
PythonClass.positional_only(arg="x")

def test_unexpected_keyword(self):
msg = "PythonClass.method() got an unexpected keyword argument 'bad'"
with self.assertRaisesRegex(TypeError, "^"+re.escape(msg)+"$"):
PYTHON_INSTANCE.method(bad="x")

def test_multiple_values(self):
kwargs = {"arg1":2, "arg2":3}
msg = "PythonClass.method() got multiple values for argument 'arg1'"
with self.assertRaisesRegex(TypeError, "^"+re.escape(msg)+"$"):
PYTHON_INSTANCE.method("arg1", "arg2", arg1="oops")


class FastCallTests(unittest.TestCase):
"""Test calling using various callables from C
"""
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