Skip to content

Commit 5c6f5c1

Browse files
committed
Suggest missing 'self' when a method is called with too many positional argumen
1 parent fcda96f commit 5c6f5c1

2 files changed

Lines changed: 76 additions & 5 deletions

File tree

Lib/test/test_call.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,12 @@ def static_no_args():
910910
def positional_only(arg, /):
911911
pass
912912

913+
def missing_self(another_arg):
914+
pass
915+
916+
def missing_self_no_args():
917+
pass
918+
913919
@cpython_only
914920
class TestErrorMessagesUseQualifiedName(unittest.TestCase):
915921

@@ -919,6 +925,16 @@ def check_raises_type_error(self, message):
919925
yield
920926
self.assertEqual(str(cm.exception), message)
921927

928+
def test_too_many_positional_but_missing_self(self):
929+
msg = "A.missing_self() takes 1 positional argument but 2 were given. Did you forget to declare 'self' as the first parameter?"
930+
with self.check_raises_type_error(msg):
931+
A().missing_self("another_arg")
932+
933+
def test_too_many_positional_but_missing_self_no_args(self):
934+
msg = "A.missing_self_no_args() takes 0 positional arguments but 1 was given. Did you forget to declare 'self' as the first parameter?"
935+
with self.check_raises_type_error(msg):
936+
A().missing_self_no_args()
937+
922938
def test_missing_arguments(self):
923939
msg = "A.method_two_args() missing 1 required positional argument: 'y'"
924940
with self.check_raises_type_error(msg):

Python/ceval.c

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,12 +1574,13 @@ missing_arguments(PyThreadState *tstate, PyCodeObject *co,
15741574
static void
15751575
too_many_positional(PyThreadState *tstate, PyCodeObject *co,
15761576
Py_ssize_t given, PyObject *defaults,
1577-
_PyStackRef *localsplus, PyObject *qualname)
1577+
_PyStackRef *localsplus, PyObject *qualname,
1578+
int suggest_missing_self)
15781579
{
15791580
int plural;
15801581
Py_ssize_t kwonly_given = 0;
15811582
Py_ssize_t i;
1582-
PyObject *sig, *kwonly_sig;
1583+
PyObject *sig, *kwonly_sig, *self_hint = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
15831584
Py_ssize_t co_argcount = co->co_argcount;
15841585

15851586
assert((co->co_flags & CO_VARARGS) == 0);
@@ -1617,18 +1618,71 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co,
16171618
kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
16181619
assert(kwonly_sig != NULL);
16191620
}
1621+
if (suggest_missing_self) {
1622+
self_hint = PyUnicode_FromString(
1623+
". Did you forget to declare 'self' as the first parameter?");
1624+
if (self_hint == NULL) {
1625+
Py_DECREF(sig);
1626+
Py_DECREF(kwonly_sig);
1627+
return;
1628+
}
1629+
}
16201630
_PyErr_Format(tstate, PyExc_TypeError,
1621-
"%U() takes %U positional argument%s but %zd%U %s given",
1631+
"%U() takes %U positional argument%s but %zd%U %s given%U",
16221632
qualname,
16231633
sig,
16241634
plural ? "s" : "",
16251635
given,
16261636
kwonly_sig,
1627-
given == 1 && !kwonly_given ? "was" : "were");
1637+
given == 1 && !kwonly_given ? "was" : "were",
1638+
self_hint
1639+
);
1640+
Py_DECREF(self_hint);
16281641
Py_DECREF(sig);
16291642
Py_DECREF(kwonly_sig);
16301643
}
16311644

1645+
static int
1646+
suggest_missing_self(PyFunctionObject *func, PyCodeObject *co,
1647+
_PyStackRef const *args, Py_ssize_t argcount)
1648+
{
1649+
if (co->co_argcount >= argcount) {
1650+
// When declared count is more than provided, there is nothing to add
1651+
return 0;
1652+
}
1653+
1654+
PyObject *self = PyStackRef_AsPyObjectBorrow(args[0]);
1655+
if (self == NULL) {
1656+
// When first arg is NULL, its not really about self
1657+
return 0;
1658+
}
1659+
1660+
Py_ssize_t qualname_len;
1661+
const char *qualname = PyUnicode_AsUTF8AndSize(
1662+
func->func_qualname, &qualname_len);
1663+
if (qualname == NULL) {
1664+
PyErr_Clear();
1665+
return 0;
1666+
}
1667+
1668+
const char *method_dot = strrchr(qualname, '.');
1669+
if (method_dot == NULL) {
1670+
return 0;
1671+
}
1672+
1673+
const char *class_start = qualname;
1674+
for (const char *p = qualname; p < method_dot; p++) {
1675+
if (*p == '.') {
1676+
class_start = p + 1;
1677+
}
1678+
}
1679+
Py_ssize_t class_len = method_dot - class_start;
1680+
const char *type_name = Py_TYPE(self)->tp_name;
1681+
1682+
return (strlen(type_name) == (size_t)class_len
1683+
&& strncmp(type_name, class_start, (size_t)class_len) == 0);
1684+
}
1685+
16321686
static int
16331687
positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co,
16341688
Py_ssize_t kwcount, PyObject* kwnames,
@@ -1721,6 +1775,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
17211775

17221776
/* Copy all positional arguments into local variables */
17231777
Py_ssize_t j, n;
1778+
int missing_self_hint = suggest_missing_self(func, co, args, argcount);
17241779
if (argcount > co->co_argcount) {
17251780
n = co->co_argcount;
17261781
}
@@ -1864,7 +1919,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
18641919
/* Check the number of positional arguments */
18651920
if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
18661921
too_many_positional(tstate, co, argcount, func->func_defaults, localsplus,
1867-
func->func_qualname);
1922+
func->func_qualname, missing_self_hint);
18681923
goto fail_post_args;
18691924
}
18701925

0 commit comments

Comments
 (0)