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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 68 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 28 additions & 3 deletions
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

Lines changed: 7 additions & 7 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy