-
-
Notifications
You must be signed in to change notification settings - Fork 34.8k
gh-152315: Add a suggestion for self #152326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7b9384f
a4c0ffe
38093f5
37e1612
5cb6906
d6ef62e
723d15a
febd510
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -910,15 +910,36 @@ def static_no_args(): | |
| def positional_only(arg, /): | ||
| pass | ||
|
|
||
| def method_with_self(self, arg, kwarg=1): | ||
| pass | ||
|
|
||
| def missing_self(another_arg): | ||
| pass | ||
|
|
||
| def missing_self_no_args(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, add a test case for and then call it like Also: you can add |
||
| pass | ||
|
|
||
| @cpython_only | ||
| class TestErrorMessagesUseQualifiedName(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_happy_path(self): | ||
| self.assertIs(None, A().method_with_self(1, kwarg=2)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no real need for this test. We test method calling in many other test cases in CPython. It does not add any real value. |
||
|
|
||
| def test_too_many_positional_but_missing_self(self): | ||
| msg = "A.missing_self() takes 1 positional argument but 2 were given. Did you forget the 'self' parameter in the function definition?" | ||
| with self.check_raises_type_error(msg): | ||
| A().missing_self("another_arg") | ||
|
|
||
| def test_too_many_positional_but_missing_self_no_args(self): | ||
| msg = "A.missing_self_no_args() takes 0 positional arguments but 1 was given. Did you forget the 'self' parameter in the function definition?" | ||
| with self.check_raises_type_error(msg): | ||
| A().missing_self_no_args() | ||
|
|
||
| def test_missing_arguments(self): | ||
| msg = "A.method_two_args() missing 1 required positional argument: 'y'" | ||
| with self.check_raises_type_error(msg): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| If number of arguments differs by one for bound methods and the number of | ||
| positional arg differs, we we add an additional hint in the TypeError: "Did | ||
| you forget the 'self' parameter in the function definition?" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1574,12 +1574,13 @@ missing_arguments(PyThreadState *tstate, PyCodeObject *co, | |
| static void | ||
| too_many_positional(PyThreadState *tstate, PyCodeObject *co, | ||
| Py_ssize_t given, PyObject *defaults, | ||
| _PyStackRef *localsplus, PyObject *qualname) | ||
| _PyStackRef *localsplus, PyObject *qualname, | ||
| int should_suggest_missing_self) | ||
| { | ||
| int plural; | ||
| Py_ssize_t kwonly_given = 0; | ||
| Py_ssize_t i; | ||
| PyObject *sig, *kwonly_sig; | ||
| PyObject *sig, *kwonly_sig, *self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR); | ||
| Py_ssize_t co_argcount = co->co_argcount; | ||
|
|
||
| assert((co->co_flags & CO_VARARGS) == 0); | ||
|
|
@@ -1617,18 +1618,40 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co, | |
| kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR); | ||
| assert(kwonly_sig != NULL); | ||
| } | ||
| if (should_suggest_missing_self) { | ||
| self_hint = PyUnicode_FromString( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't feel super strongly about this, but I disagree with refcounting immortal objects. I've expressed before that too many users rely on certain things being immortal (such as empty strings), so changing that would be very difficult anyway. We might as well enjoy the performance boost rather than trying to maintain forward compatibility with a seemingly impossible case.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm ok with not changing this path though I would prefer to have it initialized to NULL instead in this case and before constructing the message we would put it to something. But I won't be strongly opposed to keep things as is. |
||
| ". Did you forget the 'self' parameter in the function definition?"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this suggestion be also there if we are using classmethods? Or what about methodws on metaclasses? they can have |
||
| if (self_hint == NULL) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If PyUnicode_FromString fails, the original TypeError is never raised and the user gets no error message. Better to skip the hint and still emit the normal _PyErr_Format.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should actually propagate the exception in this case, and not suggest anything at all. This is already the case when creating |
||
| self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR); | ||
| } | ||
| } | ||
| _PyErr_Format(tstate, PyExc_TypeError, | ||
| "%U() takes %U positional argument%s but %zd%U %s given", | ||
| "%U() takes %U positional argument%s but %zd%U %s given%U", | ||
| qualname, | ||
| sig, | ||
| plural ? "s" : "", | ||
| given, | ||
| kwonly_sig, | ||
| given == 1 && !kwonly_given ? "was" : "were"); | ||
| given == 1 && !kwonly_given ? "was" : "were", | ||
| self_hint | ||
| ); | ||
| Py_DECREF(self_hint); | ||
| Py_DECREF(sig); | ||
| Py_DECREF(kwonly_sig); | ||
| } | ||
|
|
||
| static int | ||
| suggest_missing_self(PyFunctionObject *func, PyCodeObject *co, _PyStackRef const *args, Py_ssize_t argcount) | ||
| { | ||
| if ((co->co_argcount + 1) != argcount || argcount == 0) { | ||
| return 0; | ||
| } | ||
| PyObject *first_argument = PyStackRef_AsPyObjectBorrow(args[0]); | ||
| PyTypeObject *self_cls = Py_TYPE(first_argument); | ||
| PyFunctionObject *possibly_current_function = (PyFunctionObject *) _PyType_Lookup(self_cls, co->co_name); | ||
| return possibly_current_function == func; | ||
| } | ||
|
|
||
| static int | ||
| positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co, | ||
| Py_ssize_t kwcount, PyObject* kwnames, | ||
|
|
@@ -1721,6 +1744,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, | |
|
|
||
| /* Copy all positional arguments into local variables */ | ||
| Py_ssize_t j, n; | ||
| int missing_self_hint = suggest_missing_self(func, co, args, argcount); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The heuristic of matching tp_name against the class segment in qualname is solid. A negative test for a @classmethod missing cls would be useful, it probably shouldnt trigger the hint, and that would document the expected behavior. |
||
| if (argcount > co->co_argcount) { | ||
| n = co->co_argcount; | ||
| } | ||
|
|
@@ -1864,7 +1888,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func, | |
| /* Check the number of positional arguments */ | ||
| if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) { | ||
| too_many_positional(tstate, co, argcount, func->func_defaults, localsplus, | ||
| func->func_qualname); | ||
| func->func_qualname, missing_self_hint); | ||
| goto fail_post_args; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good setup for the happy path. If possible, add a def method_with_self(self, x) on the same class A to cover the false positive case.