diff options
-rw-r--r-- | Include/internal/pycore_pyerrors.h | 2 | ||||
-rw-r--r-- | Lib/test/test_call.py | 68 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Core and Builtins/2023-08-15-11-09-50.gh-issue-107944.zQLp3j.rst | 2 | ||||
-rw-r--r-- | Python/ceval.c | 31 | ||||
-rw-r--r-- | Python/suggestions.c | 14 |
5 files changed, 106 insertions, 11 deletions
diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h index 45929f4..91fd689 100644 --- a/Include/internal/pycore_pyerrors.h +++ b/Include/internal/pycore_pyerrors.h @@ -150,7 +150,7 @@ extern PyObject* _PyExc_PrepReraiseStar( extern int _PyErr_CheckSignalsTstate(PyThreadState *tstate); extern void _Py_DumpExtensionModules(int fd, PyInterpreterState *interp); - +extern PyObject* _Py_CalculateSuggestions(PyObject *dir, PyObject *name); extern PyObject* _Py_Offer_Suggestions(PyObject* exception); // Export for '_testinternalcapi' shared extension PyAPI_FUNC(Py_ssize_t) _Py_UTF8_Edit_Cost(PyObject *str_a, PyObject *str_b, diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index c3c3b18..008a8c1 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -916,6 +916,74 @@ class TestErrorMessagesUseQualifiedName(unittest.TestCase): A().method_two_args("x", "y", x="oops") @cpython_only +class TestErrorMessagesSuggestions(unittest.TestCase): + @contextlib.contextmanager + def check_suggestion_includes(self, message): + with self.assertRaises(TypeError) as cm: + yield + self.assertIn(f"Did you mean '{message}'?", str(cm.exception)) + + @contextlib.contextmanager + def check_suggestion_not_pressent(self): + with self.assertRaises(TypeError) as cm: + yield + self.assertNotIn("Did you mean", str(cm.exception)) + + def test_unexpected_keyword_suggestion_valid_positions(self): + def foo(blech=None, /, aaa=None, *args, late1=None): + pass + + cases = [ + ("blach", None), + ("aa", "aaa"), + ("orgs", None), + ("late11", "late1"), + ] + + for keyword, suggestion in cases: + with self.subTest(keyword): + ctx = self.check_suggestion_includes(suggestion) if suggestion else self.check_suggestion_not_pressent() + with ctx: + foo(**{keyword:None}) + + def test_unexpected_keyword_suggestion_kinds(self): + + def substitution(noise=None, more_noise=None, a = None, blech = None): + pass + + def elimination(noise = None, more_noise = None, a = None, blch = None): + pass + + def addition(noise = None, more_noise = None, a = None, bluchin = None): + pass + + def substitution_over_elimination(blach = None, bluc = None): + pass + + def substitution_over_addition(blach = None, bluchi = None): + pass + + def elimination_over_addition(bluc = None, blucha = None): + pass + + def case_change_over_substitution(BLuch=None, Luch = None, fluch = None): + pass + + for func, suggestion in [ + (addition, "bluchin"), + (substitution, "blech"), + (elimination, "blch"), + (addition, "bluchin"), + (substitution_over_elimination, "blach"), + (substitution_over_addition, "blach"), + (elimination_over_addition, "bluc"), + (case_change_over_substitution, "BLuch"), + ]: + with self.subTest(suggestion): + with self.check_suggestion_includes(suggestion): + func(bluch=None) + +@cpython_only class TestRecursion(unittest.TestCase): @skip_on_s390x diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-08-15-11-09-50.gh-issue-107944.zQLp3j.rst b/Misc/NEWS.d/next/Core and Builtins/2023-08-15-11-09-50.gh-issue-107944.zQLp3j.rst new file mode 100644 index 0000000..9a53332 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-08-15-11-09-50.gh-issue-107944.zQLp3j.rst @@ -0,0 +1,2 @@ +Improve error message for function calls with bad keyword arguments. Patch +by Pablo Galindo diff --git a/Python/ceval.c b/Python/ceval.c index 329a1a1..f7dfaeb 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -26,6 +26,7 @@ #include "pycore_tuple.h" // _PyTuple_ITEMS() #include "pycore_typeobject.h" // _PySuper_Lookup() #include "pycore_uops.h" // _PyUOpExecutorObject +#include "pycore_pyerrors.h" #include "pycore_dict.h" #include "dictobject.h" @@ -1337,9 +1338,33 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, goto kw_fail; } - _PyErr_Format(tstate, PyExc_TypeError, - "%U() got an unexpected keyword argument '%S'", - func->func_qualname, keyword); + PyObject* suggestion_keyword = NULL; + if (total_args > co->co_posonlyargcount) { + PyObject* possible_keywords = PyList_New(total_args - co->co_posonlyargcount); + + if (!possible_keywords) { + PyErr_Clear(); + } else { + for (Py_ssize_t k = co->co_posonlyargcount; k < total_args; k++) { + PyList_SET_ITEM(possible_keywords, k - co->co_posonlyargcount, co_varnames[k]); + } + + suggestion_keyword = _Py_CalculateSuggestions(possible_keywords, keyword); + Py_DECREF(possible_keywords); + } + } + + if (suggestion_keyword) { + _PyErr_Format(tstate, PyExc_TypeError, + "%U() got an unexpected keyword argument '%S'. Did you mean '%S'?", + func->func_qualname, keyword, suggestion_keyword); + Py_DECREF(suggestion_keyword); + } else { + _PyErr_Format(tstate, PyExc_TypeError, + "%U() got an unexpected keyword argument '%S'", + func->func_qualname, keyword); + } + goto kw_fail; } diff --git a/Python/suggestions.c b/Python/suggestions.c index 47aeb08..12097f7 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -126,8 +126,8 @@ levenshtein_distance(const char *a, size_t a_size, return result; } -static inline PyObject * -calculate_suggestions(PyObject *dir, +PyObject * +_Py_CalculateSuggestions(PyObject *dir, PyObject *name) { assert(!PyErr_Occurred()); @@ -195,7 +195,7 @@ get_suggestions_for_attribute_error(PyAttributeErrorObject *exc) return NULL; } - PyObject *suggestions = calculate_suggestions(dir, name); + PyObject *suggestions = _Py_CalculateSuggestions(dir, name); Py_DECREF(dir); return suggestions; } @@ -259,7 +259,7 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame) } } - PyObject *suggestions = calculate_suggestions(dir, name); + PyObject *suggestions = _Py_CalculateSuggestions(dir, name); Py_DECREF(dir); if (suggestions != NULL || PyErr_Occurred()) { return suggestions; @@ -269,7 +269,7 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame) if (dir == NULL) { return NULL; } - suggestions = calculate_suggestions(dir, name); + suggestions = _Py_CalculateSuggestions(dir, name); Py_DECREF(dir); if (suggestions != NULL || PyErr_Occurred()) { return suggestions; @@ -279,7 +279,7 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame) if (dir == NULL) { return NULL; } - suggestions = calculate_suggestions(dir, name); + suggestions = _Py_CalculateSuggestions(dir, name); Py_DECREF(dir); return suggestions; @@ -371,7 +371,7 @@ offer_suggestions_for_import_error(PyImportErrorObject *exc) return NULL; } - PyObject *suggestion = calculate_suggestions(dir, name); + PyObject *suggestion = _Py_CalculateSuggestions(dir, name); Py_DECREF(dir); if (!suggestion) { return NULL; |