Skip to content

Commit 75b3db8

Browse files
authored
gh-107944: Improve error message for function calls with bad keyword arguments (#107969)
1 parent 61c7249 commit 75b3db8

File tree

5 files changed

+106
-11
lines changed

5 files changed

+106
-11
lines changed

Include/internal/pycore_pyerrors.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ extern PyObject* _PyExc_PrepReraiseStar(
150150
extern int _PyErr_CheckSignalsTstate(PyThreadState *tstate);
151151

152152
extern void _Py_DumpExtensionModules(int fd, PyInterpreterState *interp);
153-
153+
extern PyObject* _Py_CalculateSuggestions(PyObject *dir, PyObject *name);
154154
extern PyObject* _Py_Offer_Suggestions(PyObject* exception);
155155
// Export for '_testinternalcapi' shared extension
156156
PyAPI_FUNC(Py_ssize_t) _Py_UTF8_Edit_Cost(PyObject *str_a, PyObject *str_b,

Lib/test/test_call.py

+68
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,74 @@ def test_multiple_values(self):
915915
with self.check_raises_type_error(msg):
916916
A().method_two_args("x", "y", x="oops")
917917

918+
@cpython_only
919+
class TestErrorMessagesSuggestions(unittest.TestCase):
920+
@contextlib.contextmanager
921+
def check_suggestion_includes(self, message):
922+
with self.assertRaises(TypeError) as cm:
923+
yield
924+
self.assertIn(f"Did you mean '{message}'?", str(cm.exception))
925+
926+
@contextlib.contextmanager
927+
def check_suggestion_not_pressent(self):
928+
with self.assertRaises(TypeError) as cm:
929+
yield
930+
self.assertNotIn("Did you mean", str(cm.exception))
931+
932+
def test_unexpected_keyword_suggestion_valid_positions(self):
933+
def foo(blech=None, /, aaa=None, *args, late1=None):
934+
pass
935+
936+
cases = [
937+
("blach", None),
938+
("aa", "aaa"),
939+
("orgs", None),
940+
("late11", "late1"),
941+
]
942+
943+
for keyword, suggestion in cases:
944+
with self.subTest(keyword):
945+
ctx = self.check_suggestion_includes(suggestion) if suggestion else self.check_suggestion_not_pressent()
946+
with ctx:
947+
foo(**{keyword:None})
948+
949+
def test_unexpected_keyword_suggestion_kinds(self):
950+
951+
def substitution(noise=None, more_noise=None, a = None, blech = None):
952+
pass
953+
954+
def elimination(noise = None, more_noise = None, a = None, blch = None):
955+
pass
956+
957+
def addition(noise = None, more_noise = None, a = None, bluchin = None):
958+
pass
959+
960+
def substitution_over_elimination(blach = None, bluc = None):
961+
pass
962+
963+
def substitution_over_addition(blach = None, bluchi = None):
964+
pass
965+
966+
def elimination_over_addition(bluc = None, blucha = None):
967+
pass
968+
969+
def case_change_over_substitution(BLuch=None, Luch = None, fluch = None):
970+
pass
971+
972+
for func, suggestion in [
973+
(addition, "bluchin"),
974+
(substitution, "blech"),
975+
(elimination, "blch"),
976+
(addition, "bluchin"),
977+
(substitution_over_elimination, "blach"),
978+
(substitution_over_addition, "blach"),
979+
(elimination_over_addition, "bluc"),
980+
(case_change_over_substitution, "BLuch"),
981+
]:
982+
with self.subTest(suggestion):
983+
with self.check_suggestion_includes(suggestion):
984+
func(bluch=None)
985+
918986
@cpython_only
919987
class TestRecursion(unittest.TestCase):
920988

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve error message for function calls with bad keyword arguments. Patch
2+
by Pablo Galindo

Python/ceval.c

+28-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "pycore_tuple.h" // _PyTuple_ITEMS()
2727
#include "pycore_typeobject.h" // _PySuper_Lookup()
2828
#include "pycore_uops.h" // _PyUOpExecutorObject
29+
#include "pycore_pyerrors.h"
2930

3031
#include "pycore_dict.h"
3132
#include "dictobject.h"
@@ -1337,9 +1338,33 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
13371338
goto kw_fail;
13381339
}
13391340

1340-
_PyErr_Format(tstate, PyExc_TypeError,
1341-
"%U() got an unexpected keyword argument '%S'",
1342-
func->func_qualname, keyword);
1341+
PyObject* suggestion_keyword = NULL;
1342+
if (total_args > co->co_posonlyargcount) {
1343+
PyObject* possible_keywords = PyList_New(total_args - co->co_posonlyargcount);
1344+
1345+
if (!possible_keywords) {
1346+
PyErr_Clear();
1347+
} else {
1348+
for (Py_ssize_t k = co->co_posonlyargcount; k < total_args; k++) {
1349+
PyList_SET_ITEM(possible_keywords, k - co->co_posonlyargcount, co_varnames[k]);
1350+
}
1351+
1352+
suggestion_keyword = _Py_CalculateSuggestions(possible_keywords, keyword);
1353+
Py_DECREF(possible_keywords);
1354+
}
1355+
}
1356+
1357+
if (suggestion_keyword) {
1358+
_PyErr_Format(tstate, PyExc_TypeError,
1359+
"%U() got an unexpected keyword argument '%S'. Did you mean '%S'?",
1360+
func->func_qualname, keyword, suggestion_keyword);
1361+
Py_DECREF(suggestion_keyword);
1362+
} else {
1363+
_PyErr_Format(tstate, PyExc_TypeError,
1364+
"%U() got an unexpected keyword argument '%S'",
1365+
func->func_qualname, keyword);
1366+
}
1367+
13431368
goto kw_fail;
13441369
}
13451370

Python/suggestions.c

+7-7
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ levenshtein_distance(const char *a, size_t a_size,
126126
return result;
127127
}
128128

129-
static inline PyObject *
130-
calculate_suggestions(PyObject *dir,
129+
PyObject *
130+
_Py_CalculateSuggestions(PyObject *dir,
131131
PyObject *name)
132132
{
133133
assert(!PyErr_Occurred());
@@ -195,7 +195,7 @@ get_suggestions_for_attribute_error(PyAttributeErrorObject *exc)
195195
return NULL;
196196
}
197197

198-
PyObject *suggestions = calculate_suggestions(dir, name);
198+
PyObject *suggestions = _Py_CalculateSuggestions(dir, name);
199199
Py_DECREF(dir);
200200
return suggestions;
201201
}
@@ -259,7 +259,7 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
259259
}
260260
}
261261

262-
PyObject *suggestions = calculate_suggestions(dir, name);
262+
PyObject *suggestions = _Py_CalculateSuggestions(dir, name);
263263
Py_DECREF(dir);
264264
if (suggestions != NULL || PyErr_Occurred()) {
265265
return suggestions;
@@ -269,7 +269,7 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
269269
if (dir == NULL) {
270270
return NULL;
271271
}
272-
suggestions = calculate_suggestions(dir, name);
272+
suggestions = _Py_CalculateSuggestions(dir, name);
273273
Py_DECREF(dir);
274274
if (suggestions != NULL || PyErr_Occurred()) {
275275
return suggestions;
@@ -279,7 +279,7 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
279279
if (dir == NULL) {
280280
return NULL;
281281
}
282-
suggestions = calculate_suggestions(dir, name);
282+
suggestions = _Py_CalculateSuggestions(dir, name);
283283
Py_DECREF(dir);
284284

285285
return suggestions;
@@ -371,7 +371,7 @@ offer_suggestions_for_import_error(PyImportErrorObject *exc)
371371
return NULL;
372372
}
373373

374-
PyObject *suggestion = calculate_suggestions(dir, name);
374+
PyObject *suggestion = _Py_CalculateSuggestions(dir, name);
375375
Py_DECREF(dir);
376376
if (!suggestion) {
377377
return NULL;

0 commit comments

Comments
 (0)