mirror of
https://github.com/python/cpython.git
synced 2025-08-04 17:08:35 +00:00
gh-107944: Improve error message for function calls with bad keyword arguments (#107969)
This commit is contained in:
parent
61c7249759
commit
75b3db8445
5 changed files with 106 additions and 11 deletions
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Improve error message for function calls with bad keyword arguments. Patch
|
||||
by Pablo Galindo
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue