summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPablo Galindo Salgado <Pablogsal@gmail.com>2023-08-17 18:39:42 (GMT)
committerGitHub <noreply@github.com>2023-08-17 18:39:42 (GMT)
commit75b3db8445188c2ad38cabc0021af694df0829b8 (patch)
tree0787c79b66363158b539a0de86a0b3ef6c37d0eb
parent61c7249759ce88465ea655d5c19d17d03ff3f74b (diff)
downloadcpython-75b3db8445188c2ad38cabc0021af694df0829b8.zip
cpython-75b3db8445188c2ad38cabc0021af694df0829b8.tar.gz
cpython-75b3db8445188c2ad38cabc0021af694df0829b8.tar.bz2
gh-107944: Improve error message for function calls with bad keyword arguments (#107969)
-rw-r--r--Include/internal/pycore_pyerrors.h2
-rw-r--r--Lib/test/test_call.py68
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2023-08-15-11-09-50.gh-issue-107944.zQLp3j.rst2
-rw-r--r--Python/ceval.c31
-rw-r--r--Python/suggestions.c14
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;