Skip to content

Commit 17689e3

Browse files
authored
gh-107944: Improve error message for getargs with bad keyword arguments (#114792)
1 parent 9e90313 commit 17689e3

File tree

6 files changed

+113
-29
lines changed

6 files changed

+113
-29
lines changed

Doc/whatsnew/3.13.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ Improved Error Messages
101101
variables. See also :ref:`using-on-controlling-color`.
102102
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)
103103

104+
* When an incorrect keyword argument is passed to a function, the error message
105+
now potentially suggests the correct keyword argument.
106+
(Contributed by Pablo Galindo Salgado and Shantanu Jain in :gh:`107944`.)
107+
108+
>>> "better error messages!".split(max_split=1)
109+
Traceback (most recent call last):
110+
File "<stdin>", line 1, in <module>
111+
"better error messages!".split(max_split=1)
112+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
113+
TypeError: split() got an unexpected keyword argument 'max_split'. Did you mean 'maxsplit'?
114+
104115
Other Language Changes
105116
======================
106117

Lib/test/test_call.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_varargs16_kw(self):
155155
min, 0, default=1, key=2, foo=3)
156156

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

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

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

947947
for keyword, suggestion in cases:
948948
with self.subTest(keyword):
949-
ctx = self.check_suggestion_includes(suggestion) if suggestion else self.check_suggestion_not_pressent()
949+
ctx = self.check_suggestion_includes(suggestion) if suggestion else self.check_suggestion_not_present()
950950
with ctx:
951951
foo(**{keyword:None})
952952

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

990+
def test_unexpected_keyword_suggestion_via_getargs(self):
991+
with self.check_suggestion_includes("maxsplit"):
992+
"foo".split(maxsplt=1)
993+
994+
self.assertRaisesRegex(
995+
TypeError, r"split\(\) got an unexpected keyword argument 'blech'$",
996+
"foo".split, blech=1
997+
)
998+
with self.check_suggestion_not_present():
999+
"foo".split(blech=1)
1000+
with self.check_suggestion_not_present():
1001+
"foo".split(more_noise=1, maxsplt=1)
1002+
1003+
# Also test the vgetargskeywords path
1004+
with self.check_suggestion_includes("name"):
1005+
ImportError(namez="oops")
1006+
1007+
self.assertRaisesRegex(
1008+
TypeError, r"ImportError\(\) got an unexpected keyword argument 'blech'$",
1009+
ImportError, blech=1
1010+
)
1011+
with self.check_suggestion_not_present():
1012+
ImportError(blech=1)
1013+
with self.check_suggestion_not_present():
1014+
ImportError(blech=1, namez="oops")
1015+
9901016
@cpython_only
9911017
class TestRecursion(unittest.TestCase):
9921018

Lib/test/test_capi/test_getargs.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -667,15 +667,15 @@ def test_invalid_keyword(self):
667667
try:
668668
getargs_keywords((1,2),3,arg5=10,arg666=666)
669669
except TypeError as err:
670-
self.assertEqual(str(err), "'arg666' is an invalid keyword argument for this function")
670+
self.assertEqual(str(err), "this function got an unexpected keyword argument 'arg666'")
671671
else:
672672
self.fail('TypeError should have been raised')
673673

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

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

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

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

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

780780

@@ -807,7 +807,7 @@ def test_required_args(self):
807807

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

813813

@@ -1204,7 +1204,7 @@ def test_basic(self):
12041204
"function missing required argument 'a'"):
12051205
parse((), {}, 'O', ['a'])
12061206
with self.assertRaisesRegex(TypeError,
1207-
"'b' is an invalid keyword argument"):
1207+
"this function got an unexpected keyword argument 'b'"):
12081208
parse((), {'b': 1}, '|O', ['a'])
12091209
with self.assertRaisesRegex(TypeError,
12101210
fr"argument for function given by name \('a'\) "
@@ -1278,10 +1278,10 @@ def test_nonascii_keywords(self):
12781278
fr"and position \(1\)"):
12791279
parse((1,), {name: 2}, 'O|O', [name, 'b'])
12801280
with self.assertRaisesRegex(TypeError,
1281-
f"'{name}' is an invalid keyword argument"):
1281+
f"this function got an unexpected keyword argument '{name}'"):
12821282
parse((), {name: 1}, '|O', ['b'])
12831283
with self.assertRaisesRegex(TypeError,
1284-
"'b' is an invalid keyword argument"):
1284+
"this function got an unexpected keyword argument 'b'"):
12851285
parse((), {'b': 1}, '|O', [name])
12861286

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

13071307
name2 = name.encode().decode('latin1')
13081308
if name2 != name:
13091309
with self.assertRaisesRegex(TypeError,
1310-
f"'{name2}' is an invalid keyword argument"):
1310+
f"this function got an unexpected keyword argument '{name2}'"):
13111311
parse((), {name2: 1}, '|O', [name])
13121312
name3 = name + '3'
13131313
with self.assertRaisesRegex(TypeError,
1314-
f"'{name2}' is an invalid keyword argument"):
1314+
f"this function got an unexpected keyword argument '{name2}'"):
13151315
parse((), {name2: 1, name3: 2}, '|OO', [name, name3])
13161316

13171317
def test_nested_tuple(self):

Lib/test/test_exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1917,7 +1917,7 @@ def test_attributes(self):
19171917
self.assertEqual(exc.name, 'somename')
19181918
self.assertEqual(exc.path, 'somepath')
19191919

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve error message for function calls with bad keyword arguments via getargs

Python/getargs.c

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "pycore_modsupport.h" // export _PyArg_NoKeywords()
99
#include "pycore_pylifecycle.h" // _PyArg_Fini
1010
#include "pycore_tuple.h" // _PyTuple_ITEMS()
11+
#include "pycore_pyerrors.h" // _Py_CalculateSuggestions()
1112

1213
/* Export Stable ABIs (abi only) */
1314
PyAPI_FUNC(int) _PyArg_Parse_SizeT(PyObject *, const char *, ...);
@@ -1424,12 +1425,31 @@ error_unexpected_keyword_arg(PyObject *kwargs, PyObject *kwnames, PyObject *kwtu
14241425
int match = PySequence_Contains(kwtuple, keyword);
14251426
if (match <= 0) {
14261427
if (!match) {
1427-
PyErr_Format(PyExc_TypeError,
1428-
"'%S' is an invalid keyword "
1429-
"argument for %.200s%s",
1430-
keyword,
1431-
(fname == NULL) ? "this function" : fname,
1432-
(fname == NULL) ? "" : "()");
1428+
PyObject *kwlist = PySequence_List(kwtuple);
1429+
if (!kwlist) {
1430+
return;
1431+
}
1432+
PyObject *suggestion_keyword = _Py_CalculateSuggestions(kwlist, keyword);
1433+
Py_DECREF(kwlist);
1434+
1435+
if (suggestion_keyword) {
1436+
PyErr_Format(PyExc_TypeError,
1437+
"%.200s%s got an unexpected keyword argument '%S'."
1438+
" Did you mean '%S'?",
1439+
(fname == NULL) ? "this function" : fname,
1440+
(fname == NULL) ? "" : "()",
1441+
keyword,
1442+
suggestion_keyword);
1443+
Py_DECREF(suggestion_keyword);
1444+
}
1445+
else {
1446+
PyErr_Format(PyExc_TypeError,
1447+
"%.200s%s got an unexpected keyword argument '%S'",
1448+
(fname == NULL) ? "this function" : fname,
1449+
(fname == NULL) ? "" : "()",
1450+
keyword);
1451+
}
1452+
14331453
}
14341454
return;
14351455
}
@@ -1457,6 +1477,9 @@ PyArg_ValidateKeywordArguments(PyObject *kwargs)
14571477
return 1;
14581478
}
14591479

1480+
static PyObject *
1481+
new_kwtuple(const char * const *keywords, int total, int pos);
1482+
14601483
#define IS_END_OF_FORMAT(c) (c == '\0' || c == ';' || c == ':')
14611484

14621485
static int
@@ -1722,12 +1745,35 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const char *format,
17221745
}
17231746
}
17241747
if (!match) {
1725-
PyErr_Format(PyExc_TypeError,
1726-
"'%U' is an invalid keyword "
1727-
"argument for %.200s%s",
1728-
key,
1729-
(fname == NULL) ? "this function" : fname,
1730-
(fname == NULL) ? "" : "()");
1748+
PyObject *_pykwtuple = new_kwtuple(kwlist, len, pos);
1749+
if (!_pykwtuple) {
1750+
return cleanreturn(0, &freelist);
1751+
}
1752+
PyObject *pykwlist = PySequence_List(_pykwtuple);
1753+
Py_DECREF(_pykwtuple);
1754+
if (!pykwlist) {
1755+
return cleanreturn(0, &freelist);
1756+
}
1757+
PyObject *suggestion_keyword = _Py_CalculateSuggestions(pykwlist, key);
1758+
Py_DECREF(pykwlist);
1759+
1760+
if (suggestion_keyword) {
1761+
PyErr_Format(PyExc_TypeError,
1762+
"%.200s%s got an unexpected keyword argument '%S'."
1763+
" Did you mean '%S'?",
1764+
(fname == NULL) ? "this function" : fname,
1765+
(fname == NULL) ? "" : "()",
1766+
key,
1767+
suggestion_keyword);
1768+
Py_DECREF(suggestion_keyword);
1769+
}
1770+
else {
1771+
PyErr_Format(PyExc_TypeError,
1772+
"%.200s%s got an unexpected keyword argument '%S'",
1773+
(fname == NULL) ? "this function" : fname,
1774+
(fname == NULL) ? "" : "()",
1775+
key);
1776+
}
17311777
return cleanreturn(0, &freelist);
17321778
}
17331779
}

0 commit comments

Comments
 (0)