Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-107944: Improve error message for getargs with bad keyword arguments #114792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ Improved Error Messages
variables. See also :ref:`using-on-controlling-color`.
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)

* When an incorrect keyword argument is passed to a function, the error message
now potentially suggests the correct keyword argument.
(Contributed by Pablo Galindo Salgado and Shantanu Jain in :gh:`107944`.)

>>> "better error messages!".split(max_split=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
"better error messages!".split(max_split=1)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
TypeError: split() got an unexpected keyword argument 'max_split'. Did you mean 'maxsplit'?

Other Language Changes
======================

Expand Down
32 changes: 29 additions & 3 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_varargs16_kw(self):
min, 0, default=1, key=2, foo=3)

def test_varargs17_kw(self):
msg = r"'foo' is an invalid keyword argument for print\(\)$"
msg = r"print\(\) got an unexpected keyword argument 'foo'$"
self.assertRaisesRegex(TypeError, msg,
print, 0, sep=1, end=2, file=3, flush=4, foo=5)

Expand Down Expand Up @@ -928,7 +928,7 @@ def check_suggestion_includes(self, message):
self.assertIn(f"Did you mean '{message}'?", str(cm.exception))

@contextlib.contextmanager
def check_suggestion_not_pressent(self):
def check_suggestion_not_present(self):
with self.assertRaises(TypeError) as cm:
yield
self.assertNotIn("Did you mean", str(cm.exception))
Expand All @@ -946,7 +946,7 @@ def foo(blech=None, /, aaa=None, *args, late1=None):

for keyword, suggestion in cases:
with self.subTest(keyword):
ctx = self.check_suggestion_includes(suggestion) if suggestion else self.check_suggestion_not_pressent()
ctx = self.check_suggestion_includes(suggestion) if suggestion else self.check_suggestion_not_present()
with ctx:
foo(**{keyword:None})

Expand Down Expand Up @@ -987,6 +987,32 @@ def case_change_over_substitution(BLuch=None, Luch = None, fluch = None):
with self.check_suggestion_includes(suggestion):
func(bluch=None)

def test_unexpected_keyword_suggestion_via_getargs(self):
Copy link
Member

Choose a reason for hiding this comment

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

Do you mind adding a explicit test for when the suggestion fails (the 'else' case in the conditional here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review! I added this test.

I also made a similar edit in the vgetargskeywords code path and added tests.

with self.check_suggestion_includes("maxsplit"):
"foo".split(maxsplt=1)

self.assertRaisesRegex(
TypeError, r"split\(\) got an unexpected keyword argument 'blech'$",
"foo".split, blech=1
)
with self.check_suggestion_not_present():
"foo".split(blech=1)
with self.check_suggestion_not_present():
"foo".split(more_noise=1, maxsplt=1)

# Also test the vgetargskeywords path
with self.check_suggestion_includes("name"):
ImportError(namez="oops")

self.assertRaisesRegex(
TypeError, r"ImportError\(\) got an unexpected keyword argument 'blech'$",
ImportError, blech=1
)
with self.check_suggestion_not_present():
ImportError(blech=1)
with self.check_suggestion_not_present():
ImportError(blech=1, namez="oops")

@cpython_only
class TestRecursion(unittest.TestCase):

Expand Down
26 changes: 13 additions & 13 deletions Lib/test/test_capi/test_getargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,15 +667,15 @@ def test_invalid_keyword(self):
try:
getargs_keywords((1,2),3,arg5=10,arg666=666)
except TypeError as err:
self.assertEqual(str(err), "'arg666' is an invalid keyword argument for this function")
self.assertEqual(str(err), "this function got an unexpected keyword argument 'arg666'")
else:
self.fail('TypeError should have been raised')

def test_surrogate_keyword(self):
try:
getargs_keywords((1,2), 3, (4,(5,6)), (7,8,9), **{'\uDC80': 10})
except TypeError as err:
self.assertEqual(str(err), "'\udc80' is an invalid keyword argument for this function")
self.assertEqual(str(err), "this function got an unexpected keyword argument '\udc80'")
else:
self.fail('TypeError should have been raised')

Expand Down Expand Up @@ -742,12 +742,12 @@ def test_too_many_args(self):
def test_invalid_keyword(self):
# extraneous keyword arg
with self.assertRaisesRegex(TypeError,
"'monster' is an invalid keyword argument for this function"):
"this function got an unexpected keyword argument 'monster'"):
getargs_keyword_only(1, 2, monster=666)

def test_surrogate_keyword(self):
with self.assertRaisesRegex(TypeError,
"'\udc80' is an invalid keyword argument for this function"):
"this function got an unexpected keyword argument '\udc80'"):
getargs_keyword_only(1, 2, **{'\uDC80': 10})

def test_weird_str_subclass(self):
Expand All @@ -761,7 +761,7 @@ def __hash__(self):
"invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{BadStr("keyword_only"): 3})
with self.assertRaisesRegex(TypeError,
"invalid keyword argument for this function"):
"this function got an unexpected keyword argument"):
getargs_keyword_only(1, 2, **{BadStr("monster"): 666})

def test_weird_str_subclass2(self):
Expand All @@ -774,7 +774,7 @@ def __hash__(self):
"invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{BadStr("keyword_only"): 3})
with self.assertRaisesRegex(TypeError,
"invalid keyword argument for this function"):
"this function got an unexpected keyword argument"):
getargs_keyword_only(1, 2, **{BadStr("monster"): 666})


Expand Down Expand Up @@ -807,7 +807,7 @@ def test_required_args(self):

def test_empty_keyword(self):
with self.assertRaisesRegex(TypeError,
"'' is an invalid keyword argument for this function"):
"this function got an unexpected keyword argument ''"):
self.getargs(1, 2, **{'': 666})


Expand Down Expand Up @@ -1204,7 +1204,7 @@ def test_basic(self):
"function missing required argument 'a'"):
parse((), {}, 'O', ['a'])
with self.assertRaisesRegex(TypeError,
"'b' is an invalid keyword argument"):
"this function got an unexpected keyword argument 'b'"):
parse((), {'b': 1}, '|O', ['a'])
with self.assertRaisesRegex(TypeError,
fr"argument for function given by name \('a'\) "
Expand Down Expand Up @@ -1278,10 +1278,10 @@ def test_nonascii_keywords(self):
fr"and position \(1\)"):
parse((1,), {name: 2}, 'O|O', [name, 'b'])
with self.assertRaisesRegex(TypeError,
f"'{name}' is an invalid keyword argument"):
f"this function got an unexpected keyword argument '{name}'"):
parse((), {name: 1}, '|O', ['b'])
with self.assertRaisesRegex(TypeError,
"'b' is an invalid keyword argument"):
"this function got an unexpected keyword argument 'b'"):
parse((), {'b': 1}, '|O', [name])

invalid = name.encode() + (name.encode()[:-1] or b'\x80')
Expand All @@ -1301,17 +1301,17 @@ def test_nonascii_keywords(self):
for name2 in ('b', 'ë', 'ĉ', 'Ɐ', '𐀁'):
with self.subTest(name2=name2):
with self.assertRaisesRegex(TypeError,
f"'{name2}' is an invalid keyword argument"):
f"this function got an unexpected keyword argument '{name2}'"):
parse((), {name2: 1}, '|O', [name])

name2 = name.encode().decode('latin1')
if name2 != name:
with self.assertRaisesRegex(TypeError,
f"'{name2}' is an invalid keyword argument"):
f"this function got an unexpected keyword argument '{name2}'"):
parse((), {name2: 1}, '|O', [name])
name3 = name + '3'
with self.assertRaisesRegex(TypeError,
f"'{name2}' is an invalid keyword argument"):
f"this function got an unexpected keyword argument '{name2}'"):
parse((), {name2: 1, name3: 2}, '|OO', [name, name3])

def test_nested_tuple(self):
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@ def test_attributes(self):
self.assertEqual(exc.name, 'somename')
self.assertEqual(exc.path, 'somepath')

msg = "'invalid' is an invalid keyword argument for ImportError"
msg = r"ImportError\(\) got an unexpected keyword argument 'invalid'"
with self.assertRaisesRegex(TypeError, msg):
ImportError('test', invalid='keyword')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve error message for function calls with bad keyword arguments via getargs
70 changes: 58 additions & 12 deletions Python/getargs.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "pycore_modsupport.h" // export _PyArg_NoKeywords()
#include "pycore_pylifecycle.h" // _PyArg_Fini
#include "pycore_tuple.h" // _PyTuple_ITEMS()
#include "pycore_pyerrors.h" // _Py_CalculateSuggestions()

/* Export Stable ABIs (abi only) */
PyAPI_FUNC(int) _PyArg_Parse_SizeT(PyObject *, const char *, ...);
Expand Down Expand Up @@ -1424,12 +1425,31 @@ error_unexpected_keyword_arg(PyObject *kwargs, PyObject *kwnames, PyObject *kwtu
int match = PySequence_Contains(kwtuple, keyword);
if (match <= 0) {
if (!match) {
PyErr_Format(PyExc_TypeError,
"'%S' is an invalid keyword "
"argument for %.200s%s",
keyword,
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()");
PyObject *kwlist = PySequence_List(kwtuple);
if (!kwlist) {
return;
}
PyObject *suggestion_keyword = _Py_CalculateSuggestions(kwlist, keyword);
Py_DECREF(kwlist);

if (suggestion_keyword) {
PyErr_Format(PyExc_TypeError,
"%.200s%s got an unexpected keyword argument '%S'."
" Did you mean '%S'?",
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()",
keyword,
suggestion_keyword);
Py_DECREF(suggestion_keyword);
}
else {
PyErr_Format(PyExc_TypeError,
"%.200s%s got an unexpected keyword argument '%S'",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made this error message match the one in ceval.c

(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()",
keyword);
}

}
return;
}
Expand Down Expand Up @@ -1457,6 +1477,9 @@ PyArg_ValidateKeywordArguments(PyObject *kwargs)
return 1;
}

static PyObject *
new_kwtuple(const char * const *keywords, int total, int pos);

#define IS_END_OF_FORMAT(c) (c == '\0' || c == ';' || c == ':')

static int
Expand Down Expand Up @@ -1722,12 +1745,35 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const char *format,
}
}
if (!match) {
PyErr_Format(PyExc_TypeError,
"'%U' is an invalid keyword "
"argument for %.200s%s",
key,
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()");
PyObject *_pykwtuple = new_kwtuple(kwlist, len, pos);
if (!_pykwtuple) {
return cleanreturn(0, &freelist);
}
PyObject *pykwlist = PySequence_List(_pykwtuple);
Py_DECREF(_pykwtuple);
if (!pykwlist) {
return cleanreturn(0, &freelist);
}
PyObject *suggestion_keyword = _Py_CalculateSuggestions(pykwlist, key);
Py_DECREF(pykwlist);

if (suggestion_keyword) {
PyErr_Format(PyExc_TypeError,
"%.200s%s got an unexpected keyword argument '%S'."
" Did you mean '%S'?",
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()",
key,
suggestion_keyword);
Py_DECREF(suggestion_keyword);
}
else {
PyErr_Format(PyExc_TypeError,
"%.200s%s got an unexpected keyword argument '%S'",
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()",
key);
}
return cleanreturn(0, &freelist);
}
}
Expand Down