Author: Manuel Jacob <m...@manueljacob.de>
Branch: py3.3
Changeset: r78587:cb29adb88801
Date: 2015-07-17 18:10 +0200
http://bitbucket.org/pypy/pypy/changeset/cb29adb88801/

Log:    Implement improved function call exception messages.

diff --git a/lib-python/3/test/test_extcall.py 
b/lib-python/3/test/test_extcall.py
--- a/lib-python/3/test/test_extcall.py
+++ b/lib-python/3/test/test_extcall.py
@@ -89,19 +89,19 @@
 
     >>> class Nothing: pass
     ...
-    >>> g(*Nothing())
+    >>> g(*Nothing())                     #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: g() argument after * must be a sequence, not Nothing
+    TypeError: ...argument after * must be a sequence, not Nothing
 
     >>> class Nothing:
     ...     def __len__(self): return 5
     ...
 
-    >>> g(*Nothing())
+    >>> g(*Nothing())                     #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: g() argument after * must be a sequence, not Nothing
+    TypeError: ...argument after * must be a sequence, not Nothing
 
     >>> class Nothing():
     ...     def __len__(self): return 5
@@ -153,52 +153,50 @@
       ...
     TypeError: g() got multiple values for argument 'x'
 
-    >>> f(**{1:2})
+    >>> f(**{1:2})                             #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: f() keywords must be strings
+    TypeError: ...keywords must be strings
 
     >>> h(**{'e': 2})
     Traceback (most recent call last):
       ...
     TypeError: h() got an unexpected keyword argument 'e'
 
-    >>> h(*h)
+    >>> h(*h)                                  #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: h() argument after * must be a sequence, not function
+    TypeError: ...argument after * must be a sequence, not function
 
-    >>> dir(*h)
+    >>> dir(*h)                                #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: dir() argument after * must be a sequence, not function
+    TypeError: ...argument after * must be a sequence, not function
 
-    >>> None(*h)
+    >>> None(*h)                               #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: NoneType object argument after * must be a sequence, \
-not function
+    TypeError: ...argument after * must be a sequence, not function
 
-    >>> h(**h)
+    >>> h(**h)                                 #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: h() argument after ** must be a mapping, not function
+    TypeError: ...argument after ** must be a mapping, not function
 
-    >>> dir(**h)
+    >>> dir(**h)                               #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: dir() argument after ** must be a mapping, not function
+    TypeError: ...argument after ** must be a mapping, not function
 
-    >>> None(**h)
+    >>> None(**h)                              #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: NoneType object argument after ** must be a mapping, \
-not function
+    TypeError: ...argument after ** must be a mapping, not function
 
-    >>> dir(b=1, **{'b': 1})
+    >>> dir(b=1, **{'b': 1})                   #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: dir() got multiple values for keyword argument 'b'
+    TypeError: ...got multiple values for keyword argument 'b'
 
 Another helper function
 
@@ -239,10 +237,10 @@
     ...     False
     True
 
-    >>> id(1, **{'foo': 1})
+    >>> id(1, **{'foo': 1})                 #doctest: +ELLIPSIS
     Traceback (most recent call last):
       ...
-    TypeError: id() takes no keyword arguments
+    TypeError: id() ... keyword argument...
 
 A corner case of keyword dictionary items being deleted during
 the function call setup. See <http://bugs.python.org/issue2016>.
diff --git a/pypy/interpreter/argument.py b/pypy/interpreter/argument.py
--- a/pypy/interpreter/argument.py
+++ b/pypy/interpreter/argument.py
@@ -4,6 +4,7 @@
 from rpython.rlib.debug import make_sure_not_resized
 from rpython.rlib import jit
 from rpython.rlib.objectmodel import enforceargs
+from rpython.rlib.rstring import StringBuilder
 
 from pypy.interpreter.error import OperationError, oefmt
 
@@ -210,7 +211,13 @@
             loc = co_argcount + co_kwonlyargcount
             scope_w[loc] = self.space.newtuple(starargs_w)
         elif avail > co_argcount:
-            raise ArgErrCount(avail, num_kwds, signature, defaults_w, 
w_kw_defs, 0)
+            kwonly_given = 0
+            for i in range(co_argcount, co_argcount + co_kwonlyargcount):
+                if scope_w[i] is None:
+                    kwonly_given += 1
+            raise ArgErrTooMany(signature.num_argnames(),
+                                0 if defaults_w is None else len(defaults_w),
+                                avail, kwonly_given)
 
         # if a **kwargs argument is needed, create the dict
         w_kwds = None
@@ -243,16 +250,13 @@
                             self.space, keywords, keywords_w, w_kwds,
                             kwds_mapping, self.keyword_names_w, 
self._jit_few_keywords)
                 else:
-                    if co_argcount == 0:
-                        raise ArgErrCount(avail, num_kwds, signature, 
defaults_w,
-                                          w_kw_defs, 0)
-                    
                     raise ArgErrUnknownKwds(self.space, num_remainingkwds, 
keywords,
                                             kwds_mapping, self.keyword_names_w)
 
         # check for missing arguments and fill them from the kwds,
         # or with defaults, if available
-        missing = 0
+        missing_positional = []
+        missing_kwonly = []
         if input_argcount < co_argcount + co_kwonlyargcount:
             def_first = co_argcount - (0 if defaults_w is None else 
len(defaults_w))
             j = 0
@@ -273,25 +277,26 @@
                 if defnum >= 0:
                     scope_w[i] = defaults_w[defnum]
                 else:
-                    missing += 1
+                    missing_positional.append(signature.argnames[i])
 
             # finally, fill kwonly arguments with w_kw_defs (if needed)
             for i in range(co_argcount, co_argcount + co_kwonlyargcount):
                 if scope_w[i] is not None:
                     continue
-                elif w_kw_defs is None:
-                    missing += 1
+                name = signature.kwonlyargnames[i - co_argcount]
+                if w_kw_defs is None:
+                    missing_kwonly.append(name)
                     continue
-                name = signature.kwonlyargnames[i - co_argcount]
                 w_def = self.space.finditem_str(w_kw_defs, name)
                 if w_def is not None:
                     scope_w[i] = w_def
                 else:
-                    missing += 1
+                    missing_kwonly.append(name)
 
-        if missing:
-            raise ArgErrCount(avail, num_kwds, signature,
-                              defaults_w, w_kw_defs, missing)
+        if missing_positional:
+            raise ArgErrMissing(missing_positional, True)
+        if missing_kwonly:
+            raise ArgErrMissing(missing_kwonly, False)
 
 
     def parse_into_scope(self, w_firstarg,
@@ -461,61 +466,68 @@
     def getmsg(self):
         raise NotImplementedError
 
-class ArgErrCount(ArgErr):
 
-    def __init__(self, got_nargs, nkwds, signature,
-                 defaults_w, w_kw_defs, missing_args):
-        self.signature = signature
-        self.num_defaults = 0 if defaults_w is None else len(defaults_w)
-        self.missing_args = missing_args
-        self.num_args = got_nargs
-        self.num_kwds = nkwds
+class ArgErrMissing(ArgErr):
+    def __init__(self, missing, positional):
+        self.missing = missing
+        self.positional = positional  # keyword-only otherwise
 
     def getmsg(self):
-        n = self.signature.num_argnames()
-        if n == 0:
-            msg = "takes no arguments (%d given)" % (
-                self.num_args + self.num_kwds)
+        arguments_str = StringBuilder()
+        for i, arg in enumerate(self.missing):
+            if i == 0:
+                pass
+            elif i == len(self.missing) - 1:
+                if len(self.missing) == 2:
+                    arguments_str.append(" and ")
+                else:
+                    arguments_str.append(", and ")
+            else:
+                arguments_str.append(", ")
+            arguments_str.append("'%s'" % arg)
+        msg = "missing %s required %s argument%s: %s" % (
+            len(self.missing),
+            "positional" if self.positional else "keyword-only",
+            "s" if len(self.missing) != 1 else "",
+            arguments_str.build())
+        return msg
+
+
+class ArgErrTooMany(ArgErr):
+    def __init__(self, num_args, num_defaults, given, kwonly_given):
+        self.num_args = num_args
+        self.num_defaults = num_defaults
+        self.given = given
+        self.kwonly_given = kwonly_given
+
+    def getmsg(self):
+        num_args = self.num_args
+        num_defaults = self.num_defaults
+        if num_defaults:
+            takes_str = "from %d to %d positional arguments" % (
+                num_args - num_defaults, num_args)
         else:
-            defcount = self.num_defaults
-            has_kwarg = self.signature.has_kwarg()
-            num_args = self.num_args
-            num_kwds = self.num_kwds
-            if defcount == 0 and not self.signature.has_vararg():
-                msg1 = "exactly"
-                if not has_kwarg:
-                    num_args += num_kwds
-                    num_kwds = 0
-            elif not self.missing_args:
-                msg1 = "at most"
-            else:
-                msg1 = "at least"
-                has_kwarg = False
-                n -= defcount
-            if n == 1:
-                plural = ""
-            else:
-                plural = "s"
-            if has_kwarg or num_kwds > 0:
-                msg2 = " non-keyword"
-            else:
-                msg2 = ""
-            msg = "takes %s %d%s argument%s (%d given)" % (
-                msg1,
-                n,
-                msg2,
-                plural,
-                num_args)
+            takes_str = "%d positional argument%s" % (
+                num_args, "s" if num_args != 1 else "")
+        if self.kwonly_given:
+            given_str = ("%s positional argument%s "
+                         "(and %s keyword-only argument%s) were") % (
+                self.given, "s" if self.given != 1 else "",
+                self.kwonly_given, "s" if self.kwonly_given != 1 else "")
+        else:
+            given_str = "%s %s" % (
+                self.given, "were" if self.given != 1 else "was")
+        msg = "takes %s but %s given" % (takes_str, given_str)
         return msg
 
+
 class ArgErrMultipleValues(ArgErr):
 
     def __init__(self, argname):
         self.argname = argname
 
     def getmsg(self):
-        msg = "got multiple values for keyword argument '%s'" % (
-            self.argname)
+        msg = "got multiple values for argument '%s'" % self.argname
         return msg
 
 
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, ArgErrMissing, ArgErrTooMany)
 from pypy.interpreter.signature import Signature
 from pypy.interpreter.error import OperationError
 
@@ -575,51 +575,54 @@
 
 class TestErrorHandling(object):
     def test_missing_args(self):
-        # got_nargs, nkwds, expected_nargs, has_vararg, has_kwarg,
-        # defaults_w, missing_args
-        sig = Signature([], None, None)
-        err = ArgErrCount(1, 0, sig, None, None, 0)
+        err = ArgErrMissing(['a'], True)
         s = err.getmsg()
-        assert s == "takes no arguments (1 given)"
+        assert s == "missing 1 required positional argument: 'a'"
 
-        sig = Signature(['a'], None, None)
-        err = ArgErrCount(0, 0, sig, [], None, 1)
+        err = ArgErrMissing(['a', 'b'], True)
         s = err.getmsg()
-        assert s == "takes exactly 1 argument (0 given)"
+        assert s == "missing 2 required positional arguments: 'a' and 'b'"
 
-        sig = Signature(['a', 'b'], None, None)
-        err = ArgErrCount(3, 0, sig, [], None, 0)
+        err = ArgErrMissing(['a', 'b', 'c'], True)
         s = err.getmsg()
-        assert s == "takes exactly 2 arguments (3 given)"
-        err = ArgErrCount(3, 0, sig, ['a'], None, 0)
+        assert s == "missing 3 required positional arguments: 'a', 'b', and 
'c'"
+
+        err = ArgErrMissing(['a'], False)
         s = err.getmsg()
-        assert s == "takes at most 2 arguments (3 given)"
+        assert s == "missing 1 required keyword-only argument: 'a'"
 
-        sig = Signature(['a', 'b'], '*', None)
-        err = ArgErrCount(1, 0, sig, [], None, 1)
+    def test_too_many(self):
+        err = ArgErrTooMany(0, 0, 1, 0)
         s = err.getmsg()
-        assert s == "takes at least 2 arguments (1 given)"
-        err = ArgErrCount(0, 1, sig, ['a'], None, 1)
+        assert s == "takes 0 positional arguments but 1 was given"
+
+        err = ArgErrTooMany(0, 0, 2, 0)
         s = err.getmsg()
-        assert s == "takes at least 1 non-keyword argument (0 given)"
+        assert s == "takes 0 positional arguments but 2 were given"
 
-        sig = Signature(['a'], None, '**')
-        err = ArgErrCount(2, 1, sig, [], None, 0)
+        err = ArgErrTooMany(1, 0, 2, 0)
         s = err.getmsg()
-        assert s == "takes exactly 1 non-keyword argument (2 given)"
-        err = ArgErrCount(0, 1, sig, [], None, 1)
+        assert s == "takes 1 positional argument but 2 were given"
+
+        err = ArgErrTooMany(2, 0, 3, 0)
         s = err.getmsg()
-        assert s == "takes exactly 1 non-keyword argument (0 given)"
+        assert s == "takes 2 positional arguments but 3 were given"
 
-        sig = Signature(['a'], '*', '**')
-        err = ArgErrCount(0, 1, sig, [], None, 1)
+        err = ArgErrTooMany(2, 1, 3, 0)
         s = err.getmsg()
-        assert s == "takes at least 1 non-keyword argument (0 given)"
+        assert s == "takes from 1 to 2 positional arguments but 3 were given"
 
-        sig = Signature(['a'], None, '**')
-        err = ArgErrCount(2, 1, sig, ['a'], None, 0)
+        err = ArgErrTooMany(0, 0, 1, 1)
         s = err.getmsg()
-        assert s == "takes at most 1 non-keyword argument (2 given)"
+        assert s == "takes 0 positional arguments but 1 positional argument 
(and 1 keyword-only argument) were given"
+
+        err = ArgErrTooMany(0, 0, 2, 1)
+        s = err.getmsg()
+        assert s == "takes 0 positional arguments but 2 positional arguments 
(and 1 keyword-only argument) were given"
+
+        err = ArgErrTooMany(0, 0, 1, 2)
+        s = err.getmsg()
+        assert s == "takes 0 positional arguments but 1 positional argument 
(and 2 keyword-only arguments) were given"
 
     def test_bad_type_for_star(self):
         space = self.space
@@ -665,28 +668,36 @@
     def test_multiple_values(self):
         err = ArgErrMultipleValues('bla')
         s = err.getmsg()
-        assert s == "got multiple values for keyword argument 'bla'"
+        assert s == "got multiple values for argument 'bla'"
 
 class AppTestArgument:
     def test_error_message(self):
         exc = raises(TypeError, (lambda a, b=2: 0), b=3)
-        assert str(exc.value) == "<lambda>() takes at least 1 non-keyword 
argument (0 given)"
+        assert str(exc.value) == "<lambda>() missing 1 required positional 
argument: 'a'"
         exc = raises(TypeError, (lambda: 0), b=3)
-        assert str(exc.value) == "<lambda>() takes no arguments (1 given)"
+        assert str(exc.value) == "<lambda>() got an unexpected keyword 
argument 'b'"
         exc = raises(TypeError, (lambda a, b: 0), 1, 2, 3, a=1)
-        assert str(exc.value) == "<lambda>() takes exactly 2 arguments (4 
given)"
+        assert str(exc.value) == "<lambda>() takes 2 positional arguments but 
3 were given"
         exc = raises(TypeError, (lambda a, b=1: 0), 1, 2, 3, a=1)
-        assert str(exc.value) == "<lambda>() takes at most 2 non-keyword 
arguments (3 given)"
+        assert str(exc.value) == "<lambda>() takes from 1 to 2 positional 
arguments but 3 were given"
+        exc = raises(TypeError, (lambda a, **kw: 0), 1, 2, 3)
+        assert str(exc.value) == "<lambda>() takes 1 positional argument but 3 
were given"
         exc = raises(TypeError, (lambda a, b=1, **kw: 0), 1, 2, 3)
-        assert str(exc.value) == "<lambda>() takes at most 2 non-keyword 
arguments (3 given)"
+        assert str(exc.value) == "<lambda>() takes from 1 to 2 positional 
arguments but 3 were given"
         exc = raises(TypeError, (lambda a, b, c=3, **kw: 0), 1)
-        assert str(exc.value) == "<lambda>() takes at least 2 arguments (1 
given)"
+        assert str(exc.value) == "<lambda>() missing 1 required positional 
argument: 'b'"
         exc = raises(TypeError, (lambda a, b, **kw: 0), 1)
-        assert str(exc.value) == "<lambda>() takes exactly 2 non-keyword 
arguments (1 given)"
+        assert str(exc.value) == "<lambda>() missing 1 required positional 
argument: 'b'"
         exc = raises(TypeError, (lambda a, b, c=3, **kw: 0), a=1)
-        assert str(exc.value) == "<lambda>() takes at least 2 non-keyword 
arguments (0 given)"
+        assert str(exc.value) == "<lambda>() missing 1 required positional 
argument: 'b'"
         exc = raises(TypeError, (lambda a, b, **kw: 0), a=1)
-        assert str(exc.value) == "<lambda>() takes exactly 2 non-keyword 
arguments (0 given)"
+        assert str(exc.value) == "<lambda>() missing 1 required positional 
argument: 'b'"
+        exc = raises(TypeError, '(lambda *, a: 0)()')
+        assert str(exc.value) == "<lambda>() missing 1 required keyword-only 
argument: 'a'"
+        exc = raises(TypeError, '(lambda *, a=1, b: 0)(a=1)')
+        assert str(exc.value) == "<lambda>() missing 1 required keyword-only 
argument: 'b'"
+        exc = raises(TypeError, '(lambda *, kw: 0)(1, kw=3)')
+        assert str(exc.value) == "<lambda>() takes 0 positional arguments but 
1 positional argument (and 1 keyword-only argument) were given"
 
     def test_unicode_keywords(self):
         """
_______________________________________________
pypy-commit mailing list
pypy-commit@python.org
https://mail.python.org/mailman/listinfo/pypy-commit

Reply via email to