diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 3f459222748b3a8..6d16c0da9842416 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -713,5 +713,66 @@ def test_multiple_values(self): A().method_two_args("x", "y", x="oops") +class NoSelfClass: + def foo(): + pass + + def foo1(x, y): + pass + + def foo2(self, y): + pass + + @classmethod + def clsmethod(x, y): + pass + + @staticmethod + def stmethod(x, y): + pass + + +@cpython_only +class TestErrorMessagesInMethodsGiveHintAboutSelf(unittest.TestCase): + @contextlib.contextmanager + def check_raises_type_error(self, message): + with self.assertRaises(TypeError) as cm: + yield + self.assertEqual(str(cm.exception), message) + + + def test_method_with_missing_self_no_args(self): + + msg = ("NoSelfClass.foo() takes 0 positional arguments but 1 was given. " + "Did you forget 'self' in the method definition?") + with self.check_raises_type_error(msg): + NoSelfClass().foo() + + def test_method_with_missing_self_args(self): + + msg = ("NoSelfClass.foo1() takes 2 positional arguments but 3 were given. " + "Did you forget 'self' in the method definition?") + with self.check_raises_type_error(msg): + NoSelfClass().foo1(1, 2) + + def test_method_with_self_args(self): + + msg = "NoSelfClass.foo2() takes 2 positional arguments but 3 were given" + with self.check_raises_type_error(msg): + NoSelfClass().foo2(1, 2) + + def test_method_with_classmethod(self): + + msg = "NoSelfClass.clsmethod() takes 2 positional arguments but 3 were given" + with self.check_raises_type_error(msg): + NoSelfClass().clsmethod(1, 2) + + def test_method_with_staticmethod(self): + + msg = "NoSelfClass.stmethod() takes 2 positional arguments but 3 were given" + with self.check_raises_type_error(msg): + NoSelfClass().stmethod(1, 2, 3) + + if __name__ == "__main__": unittest.main() diff --git a/Python/ceval.c b/Python/ceval.c index 5e2a160f0ed35c5..037a90633cda3b5 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -86,6 +86,7 @@ static PyObject * special_lookup(PyThreadState *, PyObject *, _Py_Identifier *); static int check_args_iterable(PyThreadState *, PyObject *func, PyObject *vararg); static void format_kwargs_error(PyThreadState *, PyObject *func, PyObject *kwargs); static void format_awaitable_error(PyThreadState *, PyTypeObject *, int, int); +static void improve_missing_self_error(PyThreadState* tstate, PyCodeObject* co); #define NAME_ERROR_MSG \ "name '%.200s' is not defined" @@ -3786,13 +3787,22 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag) We'll be passing `oparg + 1` to call_function, to make it accept the `self` as a first argument. */ + + PyObject **pfunc = (*(&sp)) - (oparg + 1) - 1; + PyObject *func = *pfunc; + PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func); res = call_function(tstate, &bounds, &sp, oparg + 1, NULL); + if (res == NULL && oparg == co->co_argcount && + PyErr_ExceptionMatches(PyExc_TypeError)) { + improve_missing_self_error(tstate, co); + } stack_pointer = sp; } PUSH(res); - if (res == NULL) + if (res == NULL) { goto error; + } DISPATCH(); } @@ -5998,3 +6008,34 @@ void Py_LeaveRecursiveCall(void) { _Py_LeaveRecursiveCall_inline(); } + +static void +improve_missing_self_error(PyThreadState* tstate, PyCodeObject* co) +{ + + PyObject *type, *val, *tb; + _PyErr_Fetch(tstate, &type, &val, &tb); + + if (!PyUnicode_CheckExact(val)) { + goto exit; + } + + if (co->co_argcount > 0 && co->co_varnames && PyTuple_GET_SIZE(co->co_varnames) > 0) { + PyObject* first_arg = PyTuple_GET_ITEM(co->co_varnames, 0); + if (_PyUnicode_EqualToASCIIString(first_arg, "self")) { + PyErr_Clear(); + goto exit; + } + } + + PyObject* new_val = PyUnicode_FromFormat("%U. Did you forget 'self' in the method" + " definition?", val); + if (new_val == NULL) { + PyErr_Clear(); + goto exit; + } + Py_DECREF(val); + val = new_val; +exit: + PyErr_Restore(type, val, tb); +}