]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-107944: Improve error message for function calls with bad keyword arguments (...
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Thu, 17 Aug 2023 18:39:42 +0000 (19:39 +0100)
committerGitHub <noreply@github.com>
Thu, 17 Aug 2023 18:39:42 +0000 (19:39 +0100)
Include/internal/pycore_pyerrors.h
Lib/test/test_call.py
Misc/NEWS.d/next/Core and Builtins/2023-08-15-11-09-50.gh-issue-107944.zQLp3j.rst [new file with mode: 0644]
Python/ceval.c
Python/suggestions.c

index 45929f40a05496eeec67d026ffd25f75ff72bb97..91fd68984523dd9533b8210bad41269a6bc7bab8 100644 (file)
@@ -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,
index c3c3b1853b573666db568534900f4d74e4dc73c4..008a8c1f0cb876e0b02b72babf722d62deba11e6 100644 (file)
@@ -915,6 +915,74 @@ class TestErrorMessagesUseQualifiedName(unittest.TestCase):
         with self.check_raises_type_error(msg):
             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):
 
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 (file)
index 0000000..9a53332
--- /dev/null
@@ -0,0 +1,2 @@
+Improve error message for function calls with bad keyword arguments. Patch
+by Pablo Galindo
index 329a1a17cf09d43ef43ef13ab3dd95569f866c29..f7dfaebcdd8f84e44addce00d22f8b6e307cb7f5 100644 (file)
@@ -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;
             }
 
index 47aeb08180f6b14ffe835619da53683d0fafe157..12097f793e3575863e59d80f2a666ca17bed93d8 100644 (file)
@@ -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;