Skip to content

Commit 0babcdd

Browse files
gh-152315: Add a suggestion for missing self parameters (GH-152326)
1 parent 5e049c3 commit 0babcdd

4 files changed

Lines changed: 201 additions & 26 deletions

File tree

‎Doc/whatsnew/3.16.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ Other language changes
9898
they reference) alive indefinitely.
9999
(Contributed by Łukasz Langa in :gh:`102960`.)
100100

101+
* :exc:`TypeError` messages for some instance methods called with too many
102+
positional arguments now suggest checking whether the method definition is
103+
missing the ``self`` parameter. (Contributed by Suren Nihalani in
104+
:gh:`152315`.)
105+
101106

102107
New modules
103108
===========

‎Lib/test/test_call.py‎

Lines changed: 142 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -947,50 +947,170 @@ def f(self, *args, **kwargs):
947947
self.assertEqual(kwargs_captured, [{"baz": "bar"}])
948948

949949
class A:
950-
def method_two_args(self, x, y):
950+
def method_two_args(arg1=None, arg2=None):
951+
pass
952+
953+
def method_one_arg(arg1):
954+
pass
955+
956+
def method_zero_arg():
957+
pass
958+
959+
@classmethod
960+
def classmethod_one_arg(arg1):
951961
pass
952962

953963
@staticmethod
954964
def static_no_args():
955965
pass
956966

957967
@staticmethod
958-
def positional_only(arg, /):
968+
def static_positional_only_one_arg(arg1, /):
969+
pass
970+
971+
@staticmethod
972+
def static_one_arg(arg1):
973+
pass
974+
975+
def method_one_arg_named_self(self):
976+
pass
977+
978+
def method_one_arg_named_cls(cls):
979+
pass
980+
981+
def method_two_arg_second_param_self(arg1, self):
982+
pass
983+
984+
985+
class AMeta(type):
986+
987+
def method_one_arg(arg1):
988+
pass
989+
990+
def method_two_arg(arg1, arg2):
991+
pass
992+
993+
@classmethod
994+
def classmethod_one_arg(arg1):
959995
pass
960996

997+
@staticmethod
998+
def static_one_arg(arg1):
999+
pass
1000+
1001+
class AClassWithMetaclass(metaclass=AMeta):
1002+
pass
1003+
9611004
@cpython_only
962-
class TestErrorMessagesUseQualifiedName(unittest.TestCase):
1005+
class TestIncorrectNumberOfPositionalArgs(unittest.TestCase):
1006+
def test_too_many_positional_with_defaults_suggests_missing_self(self):
1007+
msg = r"\.method_two_args\(\) takes from 0 to 2 positional arguments but 3 were given\. Did you forget the 'self' parameter in the function definition\?"
1008+
with self.assertRaisesRegex(TypeError, msg):
1009+
A().method_two_args("woof", "loud")
9631010

964-
@contextlib.contextmanager
965-
def check_raises_type_error(self, message):
966-
with self.assertRaises(TypeError) as cm:
967-
yield
968-
self.assertEqual(str(cm.exception), message)
1011+
def test_too_many_positional_but_missing_self_no_args(self):
1012+
msg = r"takes 0 positional arguments but 1 was given\. Did you forget the 'self' parameter in the function definition\?"
1013+
with self.assertRaisesRegex(TypeError, msg):
1014+
A().method_zero_arg()
9691015

970-
def test_missing_arguments(self):
971-
msg = "A.method_two_args() missing 1 required positional argument: 'y'"
972-
with self.check_raises_type_error(msg):
973-
A().method_two_args("x")
1016+
def test_too_many_positional_but_missing_self(self):
1017+
msg = r"\.method_one_arg\(\) takes 1 positional argument but 2 were given\. Did you forget the 'self' parameter in the function definition\?"
1018+
with self.assertRaisesRegex(TypeError, msg):
1019+
A().method_one_arg("quiet")
9741020

9751021
def test_too_many_positional(self):
976-
msg = "A.static_no_args() takes 0 positional arguments but 1 was given"
977-
with self.check_raises_type_error(msg):
1022+
msg = r"takes 0 positional arguments but 1 was given"
1023+
with self.assertRaisesRegex(TypeError, msg):
9781024
A.static_no_args("oops it's an arg")
9791025

9801026
def test_positional_only_passed_as_keyword(self):
981-
msg = "A.positional_only() got some positional-only arguments passed as keyword arguments: 'arg'"
982-
with self.check_raises_type_error(msg):
983-
A.positional_only(arg="x")
1027+
msg = r"\.static_positional_only_one_arg\(\) got some positional-only arguments passed as keyword arguments: 'arg1'"
1028+
with self.assertRaisesRegex(TypeError, msg):
1029+
A.static_positional_only_one_arg(arg1="ball")
9841030

9851031
def test_unexpected_keyword(self):
986-
msg = "A.method_two_args() got an unexpected keyword argument 'bad'"
987-
with self.check_raises_type_error(msg):
1032+
msg = r"method_two_args\(\) got an unexpected keyword argument 'bad'"
1033+
with self.assertRaisesRegex(TypeError, msg):
9881034
A().method_two_args(bad="x")
9891035

9901036
def test_multiple_values(self):
991-
msg = "A.method_two_args() got multiple values for argument 'x'"
992-
with self.check_raises_type_error(msg):
993-
A().method_two_args("x", "y", x="oops")
1037+
msg = r"\.method_two_args\(\) got multiple values for argument 'arg1'"
1038+
with self.assertRaisesRegex(TypeError, msg):
1039+
A().method_two_args("quiet", "low", arg1="oops")
1040+
1041+
def test_unbound_method_with_self_keeps_missing_argument_error(self):
1042+
msg = r"\.method_one_arg\(\) missing 1 required positional argument: 'arg1'"
1043+
with self.assertRaisesRegex(TypeError, msg):
1044+
A.method_one_arg()
1045+
1046+
def test_classmethod_missing_cls_does_not_suggest_missing_self(self):
1047+
msg = r"\.classmethod_one_arg\(\) takes 1 positional argument but 2 were given"
1048+
with self.assertRaisesRegex(TypeError, msg) as cm:
1049+
A.classmethod_one_arg("arg1")
1050+
self.assertNotIn("Did you forget", str(cm.exception))
1051+
1052+
def test_classmethod_missing_cls_via_instance_does_not_suggest_missing_self(self):
1053+
msg = r"\.classmethod_one_arg\(\) takes 1 positional argument but 2 were given"
1054+
with self.assertRaisesRegex(TypeError, msg) as cm:
1055+
A().classmethod_one_arg("poodle")
1056+
self.assertNotIn("Did you forget", str(cm.exception))
1057+
1058+
def test_staticmethod_too_many_args_does_not_suggest_missing_self(self):
1059+
msg = r"\.static_one_arg\(\) takes 1 positional argument but 2 were given"
1060+
with self.assertRaisesRegex(TypeError, msg) as cm:
1061+
A.static_one_arg(1, 2)
1062+
self.assertNotIn("Did you forget", str(cm.exception))
1063+
1064+
def test_staticmethod_too_many_args_via_instance_does_not_suggest_missing_self(self):
1065+
msg = r"\.static_one_arg\(\) takes 1 positional argument but 2 were given"
1066+
with self.assertRaisesRegex(TypeError, msg) as cm:
1067+
A().static_one_arg(1, 2)
1068+
self.assertNotIn("Did you forget", str(cm.exception))
1069+
1070+
def test_metaclass_missing_receiver_does_not_suggest_missing_self(self):
1071+
msg = r"AMeta\.method_one_arg\(\) takes 1 positional argument but 2 were given"
1072+
with self.assertRaisesRegex(TypeError, msg) as cm:
1073+
AClassWithMetaclass.method_one_arg("standard")
1074+
self.assertNotIn("Did you forget", str(cm.exception))
1075+
1076+
def test_metaclass_method_too_many_args_does_not_suggest_missing_self(self):
1077+
msg = r"AMeta\.method_two_arg\(\) takes 2 positional arguments but 3 were given"
1078+
with self.assertRaisesRegex(TypeError, msg) as cm:
1079+
AClassWithMetaclass.method_two_arg("trail", "river")
1080+
self.assertNotIn("Did you forget", str(cm.exception))
1081+
1082+
def test_metaclass_classmethod_does_not_suggest_missing_self(self):
1083+
msg = r"AMeta\.classmethod_one_arg\(\) takes 1 positional argument but 2 were given"
1084+
with self.assertRaisesRegex(TypeError, msg) as cm:
1085+
AClassWithMetaclass.classmethod_one_arg("standard")
1086+
self.assertNotIn("Did you forget", str(cm.exception))
1087+
1088+
def test_metaclass_staticmethod_does_not_suggest_missing_self(self):
1089+
msg = r"AMeta\.static_one_arg\(\) takes 1 positional argument but 2 were given"
1090+
with self.assertRaisesRegex(TypeError, msg) as cm:
1091+
AClassWithMetaclass.static_one_arg("show", "working")
1092+
self.assertNotIn("Did you forget", str(cm.exception))
1093+
1094+
def test_more_than_two_surplus_args_does_not_suggest(self):
1095+
with self.assertRaises(TypeError) as cm:
1096+
A().method_one_arg("arg1", "arg2", "arg3")
1097+
self.assertNotIn("Did you forget", str(cm.exception))
1098+
1099+
def test_when_arg_named_self_does_not_suggest(self):
1100+
with self.assertRaises(TypeError) as cm:
1101+
A().method_one_arg_named_self("arg1")
1102+
self.assertNotIn("Did you forget", str(cm.exception))
1103+
1104+
def test_when_arg_named_cls_does_not_suggest(self):
1105+
with self.assertRaises(TypeError) as cm:
1106+
A().method_one_arg_named_cls("arg1")
1107+
self.assertNotIn("Did you forget", str(cm.exception))
1108+
1109+
def test_when_second_param_is_self(self):
1110+
with self.assertRaises(TypeError) as cm:
1111+
A().method_two_arg_second_param_self()
1112+
self.assertNotIn("Did you forget", str(cm.exception))
1113+
9941114

9951115
@cpython_only
9961116
class TestErrorMessagesSuggestions(unittest.TestCase):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:exc:`TypeError` messages for some instance methods called with too many
2+
positional arguments now suggest checking whether the method definition is
3+
missing the ``self`` parameter.

‎Python/ceval.c‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,12 +1607,14 @@ missing_arguments(PyThreadState *tstate, PyCodeObject *co,
16071607
static void
16081608
too_many_positional(PyThreadState *tstate, PyCodeObject *co,
16091609
Py_ssize_t given, PyObject *defaults,
1610-
_PyStackRef *localsplus, PyObject *qualname)
1610+
_PyStackRef *localsplus, PyObject *qualname,
1611+
int should_suggest_missing_self)
16111612
{
16121613
int plural;
16131614
Py_ssize_t kwonly_given = 0;
16141615
Py_ssize_t i;
16151616
PyObject *sig, *kwonly_sig;
1617+
const char *self_hint = "";
16161618
Py_ssize_t co_argcount = co->co_argcount;
16171619

16181620
assert((co->co_flags & CO_VARARGS) == 0);
@@ -1650,18 +1652,55 @@ too_many_positional(PyThreadState *tstate, PyCodeObject *co,
16501652
kwonly_sig = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
16511653
assert(kwonly_sig != NULL);
16521654
}
1655+
if (should_suggest_missing_self) {
1656+
self_hint = ". Did you forget the 'self' parameter "
1657+
"in the function definition?";
1658+
}
16531659
_PyErr_Format(tstate, PyExc_TypeError,
1654-
"%U() takes %U positional argument%s but %zd%U %s given",
1660+
"%U() takes %U positional argument%s but %zd%U %s given%s",
16551661
qualname,
16561662
sig,
16571663
plural ? "s" : "",
16581664
given,
16591665
kwonly_sig,
1660-
given == 1 && !kwonly_given ? "was" : "were");
1666+
given == 1 && !kwonly_given ? "was" : "were",
1667+
self_hint
1668+
);
16611669
Py_DECREF(sig);
16621670
Py_DECREF(kwonly_sig);
16631671
}
16641672

1673+
static int
1674+
suggest_missing_self(PyFunctionObject *func, PyCodeObject *co,
1675+
PyObject *first_argument, Py_ssize_t argcount)
1676+
{
1677+
/* Missing self shows up as exactly one extra positional argument. */
1678+
if ((co->co_argcount + 1) != argcount) {
1679+
return 0;
1680+
}
1681+
1682+
if (first_argument == NULL || PyType_Check(first_argument)) {
1683+
/* When first arg is NULL, it's not really about self.
1684+
If it's a type object, then it's a classmethod. */
1685+
return 0;
1686+
}
1687+
1688+
if (co->co_argcount > 0) {
1689+
PyObject *first_parameter_name = PyTuple_GET_ITEM(co->co_localsplusnames, 0);
1690+
/* If the receiver parameter is already declared, another hint would be misleading. */
1691+
if (PyUnicode_CompareWithASCIIString(first_parameter_name, "self") == 0 ||
1692+
PyUnicode_CompareWithASCIIString(first_parameter_name, "cls") == 0)
1693+
{
1694+
return 0;
1695+
}
1696+
}
1697+
/* If the current function matches on the type, it's likely worth adding the hint. */
1698+
PyTypeObject *self_cls = Py_TYPE(first_argument);
1699+
PyFunctionObject *possibly_current_function =
1700+
(PyFunctionObject *)_PyType_Lookup(self_cls, co->co_name);
1701+
return possibly_current_function == func;
1702+
}
1703+
16651704
static int
16661705
positional_only_passed_as_keyword(PyThreadState *tstate, PyCodeObject *co,
16671706
Py_ssize_t kwcount, PyObject* kwnames,
@@ -1736,6 +1775,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
17361775
/* Create a dictionary for keyword parameters (**kwags) */
17371776
PyObject *kwdict;
17381777
Py_ssize_t i;
1778+
PyObject *first_argument = NULL;
17391779
if (co->co_flags & CO_VARKEYWORDS) {
17401780
kwdict = PyDict_New();
17411781
if (kwdict == NULL) {
@@ -1752,6 +1792,10 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
17521792
kwdict = NULL;
17531793
}
17541794

1795+
if (argcount > co->co_argcount && !(co->co_flags & CO_VARARGS)) {
1796+
first_argument = PyStackRef_AsPyObjectNew(args[0]);
1797+
}
1798+
17551799
/* Copy all positional arguments into local variables */
17561800
Py_ssize_t j, n;
17571801
if (argcount > co->co_argcount) {
@@ -1896,8 +1940,9 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
18961940

18971941
/* Check the number of positional arguments */
18981942
if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
1943+
int missing_self_hint = suggest_missing_self(func, co, first_argument, argcount);
18991944
too_many_positional(tstate, co, argcount, func->func_defaults, localsplus,
1900-
func->func_qualname);
1945+
func->func_qualname, missing_self_hint);
19011946
goto fail_post_args;
19021947
}
19031948

@@ -1956,6 +2001,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
19562001
goto fail_post_args;
19572002
}
19582003
}
2004+
Py_XDECREF(first_argument);
19592005
return 0;
19602006

19612007
fail_pre_positional:
@@ -1972,6 +2018,7 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
19722018
}
19732019
/* fall through */
19742020
fail_post_args:
2021+
Py_XDECREF(first_argument);
19752022
return -1;
19762023
}
19772024

0 commit comments

Comments
 (0)