[pypy-commit] pypy better-error-missing-self: improve the error message when the programmer forgets the "self" parameter of a

cfbolz pypy.commits at gmail.com
Mon Oct 3 13:55:19 EDT 2016


Author: Carl Friedrich Bolz <cfbolz at gmx.de>
Branch: better-error-missing-self
Changeset: r87548:e664458f25d0
Date: 2016-10-03 19:04 +0200
http://bitbucket.org/pypy/pypy/changeset/e664458f25d0/

Log:	improve the error message when the programmer forgets the "self"
	parameter of a method.

	(in progress)

diff --git a/pypy/interpreter/argument.py b/pypy/interpreter/argument.py
--- a/pypy/interpreter/argument.py
+++ b/pypy/interpreter/argument.py
@@ -21,7 +21,8 @@
     ###  Construction  ###
 
     def __init__(self, space, args_w, keywords=None, keywords_w=None,
-                 w_stararg=None, w_starstararg=None, keyword_names_w=None):
+                 w_stararg=None, w_starstararg=None, keyword_names_w=None,
+                 methodcall=False):
         self.space = space
         assert isinstance(args_w, list)
         self.arguments_w = args_w
@@ -41,6 +42,9 @@
         # a flag that specifies whether the JIT can unroll loops that operate
         # on the keywords
         self._jit_few_keywords = self.keywords is None or jit.isconstant(len(self.keywords))
+        # a flag whether this is likely a method call, which doesn't change the
+        # behaviour but produces better error messages
+        self.methodcall = methodcall
 
     def __repr__(self):
         """ NOT_RPYTHON """
@@ -207,7 +211,7 @@
                 starargs_w = []
             scope_w[co_argcount] = self.space.newtuple(starargs_w)
         elif avail > co_argcount:
-            raise ArgErrCount(avail, num_kwds, signature, defaults_w, 0)
+            raise self.argerrcount(avail, num_kwds, signature, defaults_w, 0)
 
         # if a **kwargs argument is needed, create the dict
         w_kwds = None
@@ -241,7 +245,7 @@
                             kwds_mapping, self.keyword_names_w, self._jit_few_keywords)
                 else:
                     if co_argcount == 0:
-                        raise ArgErrCount(avail, num_kwds, signature, defaults_w, 0)
+                        raise self.argerrcount(avail, num_kwds, signature, defaults_w, 0)
                     raise ArgErrUnknownKwds(self.space, num_remainingkwds, keywords,
                                             kwds_mapping, self.keyword_names_w)
 
@@ -265,9 +269,12 @@
                 else:
                     missing += 1
             if missing:
-                raise ArgErrCount(avail, num_kwds, signature, defaults_w, missing)
+                raise self.argerrcount(avail, num_kwds, signature, defaults_w, missing)
 
-
+    def argerrcount(self, *args):
+        if self.methodcall:
+            return ArgErrCountMethod(*args)
+        return ArgErrCount(*args)
 
     def parse_into_scope(self, w_firstarg,
                          scope_w, fnname, signature, defaults_w=None):
@@ -478,6 +485,22 @@
                 num_args)
         return msg
 
+class ArgErrCountMethod(ArgErrCount):
+    """ A subclass of ArgErrCount that is used if the argument matching is done
+    as part of a method call, in which case more information is added to the
+    error message, if the cause of the error is likely a forgotten `self`
+    argument.
+    """
+
+    def getmsg(self):
+        msg = ArgErrCount.getmsg(self)
+        n = self.signature.num_argnames()
+        if (self.num_args == n + 1 and
+                (n == 0 or self.signature.argnames[0] != "self")):
+            msg += ". Did you forget 'self' in the function definition?"
+        return msg
+
+
 class ArgErrMultipleValues(ArgErr):
 
     def __init__(self, argname):
diff --git a/pypy/interpreter/baseobjspace.py b/pypy/interpreter/baseobjspace.py
--- a/pypy/interpreter/baseobjspace.py
+++ b/pypy/interpreter/baseobjspace.py
@@ -1115,7 +1115,8 @@
         args = Arguments(self, list(args_w))
         return self.call_args(w_func, args)
 
-    def call_valuestack(self, w_func, nargs, frame):
+    def call_valuestack(self, w_func, nargs, frame, methodcall=False):
+        # methodcall is only used for better error messages in argument.py
         from pypy.interpreter.function import Function, Method, is_builtin_code
         if frame.get_is_being_profiled() and is_builtin_code(w_func):
             # XXX: this code is copied&pasted :-( from the slow path below
@@ -1138,11 +1139,12 @@
                     w_func = w_func.w_function
 
             if isinstance(w_func, Function):
-                return w_func.funccall_valuestack(nargs, frame)
+                return w_func.funccall_valuestack(
+                        nargs, frame, methodcall=methodcall)
             # end of hack for performance
 
         args = frame.make_arguments(nargs)
-        return self.call_args(w_func, args)
+        return self.call_args(w_func, args) # YYY
 
     def call_args_and_c_profile(self, frame, w_func, args):
         ec = self.getexecutioncontext()
diff --git a/pypy/interpreter/function.py b/pypy/interpreter/function.py
--- a/pypy/interpreter/function.py
+++ b/pypy/interpreter/function.py
@@ -117,7 +117,8 @@
                                               list(args_w[1:])))
         return self.call_args(Arguments(self.space, list(args_w)))
 
-    def funccall_valuestack(self, nargs, frame): # speed hack
+    def funccall_valuestack(self, nargs, frame, methodcall=False): # speed hack
+        # methodcall is only for better error messages
         from pypy.interpreter import gateway
         from pypy.interpreter.pycode import PyCode
 
@@ -164,7 +165,7 @@
             args = frame.make_arguments(nargs-1)
             return code.funcrun_obj(self, w_obj, args)
 
-        args = frame.make_arguments(nargs)
+        args = frame.make_arguments(nargs, methodcall=methodcall)
         return self.call_args(args)
 
     @jit.unroll_safe
diff --git a/pypy/interpreter/pyframe.py b/pypy/interpreter/pyframe.py
--- a/pypy/interpreter/pyframe.py
+++ b/pypy/interpreter/pyframe.py
@@ -403,11 +403,14 @@
             depth -= 1
         self.valuestackdepth = finaldepth
 
-    def make_arguments(self, nargs):
-        return Arguments(self.space, self.peekvalues(nargs))
+    def make_arguments(self, nargs, methodcall=False):
+        return Arguments(
+                self.space, self.peekvalues(nargs), methodcall=methodcall)
 
-    def argument_factory(self, arguments, keywords, keywords_w, w_star, w_starstar):
-        return Arguments(self.space, arguments, keywords, keywords_w, w_star, w_starstar)
+    def argument_factory(self, arguments, keywords, keywords_w, w_star, w_starstar, methodcall=False):
+        return Arguments(
+                self.space, arguments, keywords, keywords_w, w_star,
+                w_starstar, methodcall=methodcall)
 
     @jit.dont_look_inside
     def descr__reduce__(self, space):
diff --git a/pypy/interpreter/test/test_argument.py b/pypy/interpreter/test/test_argument.py
--- a/pypy/interpreter/test/test_argument.py
+++ b/pypy/interpreter/test/test_argument.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 import py
 from pypy.interpreter.argument import (Arguments, ArgErr, ArgErrUnknownKwds,
-        ArgErrMultipleValues, ArgErrCount)
+        ArgErrMultipleValues, ArgErrCount, ArgErrCountMethod)
 from pypy.interpreter.signature import Signature
 from pypy.interpreter.error import OperationError
 
@@ -573,6 +573,10 @@
         s = err.getmsg()
         assert s == "takes exactly 1 argument (0 given)"
 
+        sig = Signature(['self', 'b'], None, None)
+        err = ArgErrCount(3, 0, sig, [], 0)
+        s = err.getmsg()
+        assert s == "takes exactly 2 arguments (3 given)"
         sig = Signature(['a', 'b'], None, None)
         err = ArgErrCount(3, 0, sig, [], 0)
         s = err.getmsg()
@@ -607,6 +611,57 @@
         s = err.getmsg()
         assert s == "takes at most 1 non-keyword argument (2 given)"
 
+    def test_missing_args_method(self):
+        # got_nargs, nkwds, expected_nargs, has_vararg, has_kwarg,
+        # defaults_w, missing_args
+        sig = Signature([], None, None)
+        err = ArgErrCountMethod(1, 0, sig, None, 0)
+        s = err.getmsg()
+        assert s == "takes no arguments (1 given). Did you forget 'self' in the function definition?"
+
+        sig = Signature(['a'], None, None)
+        err = ArgErrCountMethod(0, 0, sig, [], 1)
+        s = err.getmsg()
+        assert s == "takes exactly 1 argument (0 given)"
+
+        sig = Signature(['self', 'b'], None, None)
+        err = ArgErrCountMethod(3, 0, sig, [], 0)
+        s = err.getmsg()
+        assert s == "takes exactly 2 arguments (3 given)"
+        sig = Signature(['a', 'b'], None, None)
+        err = ArgErrCountMethod(3, 0, sig, [], 0)
+        s = err.getmsg()
+        assert s == "takes exactly 2 arguments (3 given). Did you forget 'self' in the function definition?"
+        err = ArgErrCountMethod(3, 0, sig, ['a'], 0)
+        s = err.getmsg()
+        assert s == "takes at most 2 arguments (3 given). Did you forget 'self' in the function definition?"
+
+        sig = Signature(['a', 'b'], '*', None)
+        err = ArgErrCountMethod(1, 0, sig, [], 1)
+        s = err.getmsg()
+        assert s == "takes at least 2 arguments (1 given)"
+        err = ArgErrCountMethod(0, 1, sig, ['a'], 1)
+        s = err.getmsg()
+        assert s == "takes at least 1 non-keyword argument (0 given)"
+
+        sig = Signature(['a'], None, '**')
+        err = ArgErrCountMethod(2, 1, sig, [], 0)
+        s = err.getmsg()
+        assert s == "takes exactly 1 non-keyword argument (2 given). Did you forget 'self' in the function definition?"
+        err = ArgErrCountMethod(0, 1, sig, [], 1)
+        s = err.getmsg()
+        assert s == "takes exactly 1 non-keyword argument (0 given)"
+
+        sig = Signature(['a'], '*', '**')
+        err = ArgErrCountMethod(0, 1, sig, [], 1)
+        s = err.getmsg()
+        assert s == "takes at least 1 non-keyword argument (0 given)"
+
+        sig = Signature(['a'], None, '**')
+        err = ArgErrCountMethod(2, 1, sig, ['a'], 0)
+        s = err.getmsg()
+        assert s == "takes at most 1 non-keyword argument (2 given). Did you forget 'self' in the function definition?"
+
     def test_bad_type_for_star(self):
         space = self.space
         try:
@@ -674,6 +729,22 @@
         exc = raises(TypeError, (lambda a, b, **kw: 0), a=1)
         assert exc.value.message == "<lambda>() takes exactly 2 non-keyword arguments (0 given)"
 
+    def test_error_message_method(self):
+        class A(object):
+            def f0():
+                pass
+            def f1(a):
+                pass
+        exc = raises(TypeError, lambda : A().f0())
+        assert exc.value.message == "f0() takes no arguments (1 given). Did you forget 'self' in the function definition?"
+        exc = raises(TypeError, lambda : A().f1(1))
+        assert exc.value.message == "f1() takes exactly 1 argument (2 given). Did you forget 'self' in the function definition?"
+        def f0():
+            pass
+        exc = raises(TypeError, f0, 1)
+        assert exc.value.message == "f0() takes no arguments (1 given)"
+
+
     def test_unicode_keywords(self):
         def f(**kwargs):
             assert kwargs[u"美"] == 42
diff --git a/pypy/objspace/std/callmethod.py b/pypy/objspace/std/callmethod.py
--- a/pypy/objspace/std/callmethod.py
+++ b/pypy/objspace/std/callmethod.py
@@ -93,7 +93,8 @@
     if not n_kwargs:
         w_callable = f.peekvalue(n_args + (2 * n_kwargs) + 1)
         try:
-            w_result = f.space.call_valuestack(w_callable, n, f)
+            w_result = f.space.call_valuestack(
+                    w_callable, n, f, methodcall=True)
         finally:
             f.dropvalues(n_args + 2)
     else:
@@ -110,7 +111,8 @@
             keywords_w[n_kwargs] = w_value
 
         arguments = f.popvalues(n)    # includes w_self if it is not None
-        args = f.argument_factory(arguments, keywords, keywords_w, None, None)
+        args = f.argument_factory(
+                arguments, keywords, keywords_w, None, None, methodcall=True)
         if w_self is None:
             f.popvalue()    # removes w_self, which is None
         w_callable = f.popvalue()


More information about the pypy-commit mailing list