Commits

Benjamin Peterson committed 2978e53

allow non-ascii keyword arguments to be passed through functions (fixes #751)

  • Participants
  • Parent commits 0255d27

Comments (0)

Files changed (2)

pypy/interpreter/argument.py

         assert isinstance(args_w, list)
         self.arguments_w = args_w
         self.keywords = keywords
+        self.lexical_keywords = len(keywords) if keywords is not None else 0
         self.keywords_w = keywords_w
+        self.keyword_names_w = None
         if keywords is not None:
             assert keywords_w is not None
             assert len(keywords_w) == len(keywords)
                 raise
             keys_w = space.unpackiterable(w_keys)
         if keys_w:
+            self.keyword_names_w = keys_w
             self._do_combine_starstarargs_wrapped(keys_w, w_starstararg)
             return True
         else:
                         space.w_TypeError,
                         space.wrap("keywords must be strings"))
                 if e.match(space, space.w_UnicodeEncodeError):
-                    raise OperationError(
-                        space.w_TypeError,
-                        space.wrap("keyword cannot be encoded to ascii"))
-                raise
-            if self.keywords and key in self.keywords:
+                    # Allow this to pass through
+                    key = None
+                else:
+                    raise
+            if key is not None and self.keywords and key in self.keywords:
                 raise operationerrfmt(self.space.w_TypeError,
                                       "got multiple values "
                                       "for keyword argument "
             used_keywords = [False] * num_kwds
             for i in range(num_kwds):
                 name = keywords[i]
+                # If name was not encoded as a string, it could be None. In that
+                # case, it's definitely not going to be in the signature.
+                if name is None:
+                    continue
                 j = signature.find_argname(name)
                 if j < 0:
                     continue
             if num_remainingkwds:
                 for i in range(len(keywords)):
                     if not used_keywords[i]:
-                        key = keywords[i]
-                        self.space.setitem(w_kwds, self.space.wrap(key), keywords_w[i])
+                        if i < self.lexical_keywords:
+                            w_key = self.space.wrap(keywords[i])
+                        else:
+                            j = i - self.lexical_keywords
+                            w_key = self.keyword_names_w[j]
+                        self.space.setitem(w_kwds, w_key, keywords_w[i])
             scope_w[co_argcount + has_vararg] = w_kwds
         elif num_remainingkwds:
             if co_argcount == 0:
                 raise ArgErrCount(avail, num_kwds,
                               co_argcount, has_vararg, has_kwarg,
                               defaults_w, missing)
-            raise ArgErrUnknownKwds(num_remainingkwds, keywords, used_keywords)
+            raise ArgErrUnknownKwds(self.space, num_remainingkwds, keywords,
+                                    self.keyword_names_w, self.lexical_keywords,
+                                    used_keywords)
 
         if missing:
             raise ArgErrCount(avail, num_kwds,
 
 class ArgErrUnknownKwds(ArgErr):
 
-    def __init__(self, num_remainingkwds, keywords, used_keywords):
+    def __init__(self, space, num_remainingkwds, keywords, keyword_names_w,
+                 lexical_keywords, used_keywords):
         self.kwd_name = ''
         self.num_kwds = num_remainingkwds
         if num_remainingkwds == 1:
             for i in range(len(keywords)):
                 if not used_keywords[i]:
-                    self.kwd_name = keywords[i]
+                    if i < lexical_keywords:
+                        name = keywords[i]
+                    else:
+                        w_name = keyword_names_w[i - lexical_keywords]
+                        if not space.isinstance_w(w_name, space.w_str):
+                            # We'll assume it's unicode. Encode it.
+                            w_enc = space.wrap(space.sys.defaultencoding)
+                            w_err = space.wrap("replace")
+                            w_name = space.call_method(w_name, "encode", w_enc,
+                                                       w_err)
+                        name = space.str_w(w_name)
+                    self.kwd_name = name
                     break
 
     def getmsg(self, fnname):

pypy/interpreter/test/test_argument.py

+# -*- coding: utf-8 -*-
 import py
 from pypy.interpreter.argument import (Arguments, ArgumentsForTranslation,
     ArgErr, ArgErrUnknownKwds, ArgErrMultipleValues, ArgErrCount, rawshape,
     w_AttributeError = AttributeError
     w_UnicodeEncodeError = UnicodeEncodeError
     w_dict = dict
+    w_str = str
 
 class TestArgumentsNormal(object):
 
         args._match_signature(None, l, Signature(['abc']))
         assert len(l) == 1
         assert l[0] == space.wrap(5)
-        #
-        def str_w(w):
-            try:
-                return str(w)
-            except UnicodeEncodeError:
-                raise OperationError(space.w_UnicodeEncodeError,
-                                     space.wrap("oups"))
-        space.str_w = str_w
-        w_starstar = space.wrap({u'\u1234': 5})
-        err = py.test.raises(OperationError, Arguments,
-                             space, [], w_starstararg=w_starstar)
-        # Check that we get a TypeError.  On CPython it is because of
-        # "no argument called '?'".  On PyPy we get a TypeError too, but
-        # earlier: "keyword cannot be encoded to ascii".  The
-        # difference, besides the error message, is only apparent if the
-        # receiver also takes a **arg.  Then CPython passes the
-        # non-ascii unicode unmodified, whereas PyPy complains.  We will
-        # not care until someone has a use case for that.
-        assert not err.value.match(space, space.w_UnicodeEncodeError)
-        assert     err.value.match(space, space.w_TypeError)
 
 class TestErrorHandling(object):
     def test_missing_args(self):
             assert 0, "did not raise"
 
     def test_unknown_keywords(self):
-        err = ArgErrUnknownKwds(1, ['a', 'b'], [True, False])
+        space = DummySpace()
+        err = ArgErrUnknownKwds(space, 1, ['a', 'b'], None, 2, [True, False])
         s = err.getmsg('foo')
         assert s == "foo() got an unexpected keyword argument 'b'"
-        err = ArgErrUnknownKwds(2, ['a', 'b', 'c'], [True, False, False])
+        err = ArgErrUnknownKwds(space, 2, ['a', 'b', 'c'], None, 3,
+                                [True, False, False])
         s = err.getmsg('foo')
         assert s == "foo() got 2 unexpected keyword arguments"
 
         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_unicode_keywords(self):
+        def f(**kwargs):
+            assert kwargs[u"美"] == 42
+        f(**{u"美" : 42})
+        def f(x): pass
+        e = raises(TypeError, "f(**{u'ü' : 19})")
+        assert "?" in str(e.value)
+
 def make_arguments_for_translation(space, args_w, keywords_w={},
                                    w_stararg=None, w_starstararg=None):
     return ArgumentsForTranslation(space, args_w, keywords_w.keys(),