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