Commits

Mike Bayer committed dcf8a50

- can't coerce to unicode key by default because cache backends like
DBM don't accept unicode keys in Python 2.x. So instead have added
a new argument ``to_str`` which will allow the "unicode" function
to be substituted. On Py3K this isn't needed.

  • Participants
  • Parent commits d2822df

Comments (0)

Files changed (6)

docs/build/changelog.rst

     .. change::
         :tags: bug
 
-      :meth:`.CacheRegion.cache_on_arguments` can now accept
-      non-ascii unicode arguments and correctly produce a concatenated
-      cache key.  Courtesy Lx Yu.
+      :meth:`.CacheRegion.cache_on_arguments` now has a new argument
+      ``to_str``, defaults to ``str()``.  Can be replaced with ``unicode()``
+      or other functions to support caching of functions that
+      accept non-unicode arguments.  Initial patch courtesy Lx Yu.
 
     .. change::
         :tags: feature

dogpile/cache/compat.py

 import sys
 
 
+py2k = sys.version_info < (3, 0)
 py3k = sys.version_info >= (3, 0)
 py32 = sys.version_info >= (3, 2)
 jython = sys.platform.startswith('java')

dogpile/cache/region.py

      return a new function that generates the key based on
      the given arguments.  Such as::
 
-        def my_key_generator(namespace, fn):
+        def my_key_generator(namespace, fn, **kw):
             fname = fn.__name__
             def generate_key(*arg):
                 return namespace + "_" + fname + "_".join(str(s) for s in arg)
      :meth:`.CacheRegion.cache_multi_on_arguments`. Generated function
      should return list of keys. For example::
 
-        def my_multi_key_generator(namespace, fn):
+        def my_multi_key_generator(namespace, fn, **kw):
             namespace = fn.__name__ + (namespace or '')
 
             def generate_keys(*args):
         self.backend.delete_multi(keys)
 
 
-    def cache_on_arguments(self, namespace=None, expiration_time=None,
-                                        should_cache_fn=None):
+    def cache_on_arguments(self, namespace=None,
+                                    expiration_time=None,
+                                    should_cache_fn=None,
+                                    to_str=compat.string_type):
         """A function decorator that will cache the return
         value of the function using a key derived from the
         function itself and its arguments.
 
           .. versionadded:: 0.4.3
 
+        :param to_str: callable, will be called on each function argument
+         in order to convert to a string.  Defaults to ``str()``.  If the
+         function accepts non-ascii unicode arguments on Python 2.x, the
+         ``unicode()`` builtin can be substituted, but note this will
+         produce unicode cache keys which may require key mangling before
+         reaching the cache.
+
+          .. versionadded:: 0.5.0
+
         .. seealso::
 
             :meth:`.CacheRegion.cache_multi_on_arguments`
         """
         expiration_time_is_callable = compat.callable(expiration_time)
         def decorator(fn):
-            key_generator = self.function_key_generator(namespace, fn)
+            if to_str is compat.string_type:
+                # backwards compatible
+                key_generator = self.function_key_generator(namespace, fn)
+            else:
+                key_generator = self.function_key_generator(namespace, fn,
+                                    to_str=to_str)
             @wraps(fn)
             def decorate(*arg, **kw):
                 key = key_generator(*arg, **kw)
 
     def cache_multi_on_arguments(self, namespace=None, expiration_time=None,
                                         should_cache_fn=None,
-                                        asdict=False):
+                                        asdict=False, to_str=compat.string_type):
         """A function decorator that will cache multiple return
         values from the function using a sequence of keys derived from the
         function itself and the arguments passed to it.
          When ``asdict==True`` if the dictionary returned by the decorated
          function is missing keys, those keys will not be cached.
 
+        :param to_str: callable, will be called on each function argument
+         in order to convert to a string.  Defaults to ``str()``.  If the
+         function accepts non-ascii unicode arguments on Python 2.x, the
+         ``unicode()`` builtin can be substituted, but note this will
+         produce unicode cache keys which may require key mangling before
+         reaching the cache.
+
         .. versionadded:: 0.5.0
 
         .. seealso::
         """
         expiration_time_is_callable = compat.callable(expiration_time)
         def decorator(fn):
-            key_generator = self.function_multi_key_generator(namespace, fn)
+            key_generator = self.function_multi_key_generator(namespace, fn,
+                                            to_str=to_str)
             @wraps(fn)
             def decorate(*arg, **kw):
                 cache_keys = arg

dogpile/cache/util.py

         self.impls[name] = load
 
 
-def function_key_generator(namespace, fn):
+def function_key_generator(namespace, fn, to_str=compat.string_type):
     """Return a function that generates a string
     key, based on a given function as well as
     arguments to the returned function itself.
                     "function does not accept keyword arguments.")
         if has_self:
             args = args[1:]
-        return namespace + "|" + " ".join(map(compat.text_type, args))
+
+        return namespace + "|" + " ".join(map(to_str, args))
     return generate_key
 
-def function_multi_key_generator(namespace, fn):
+def function_multi_key_generator(namespace, fn, to_str=compat.string_type):
 
     if namespace is None:
         namespace = '%s:%s' % (fn.__module__, fn.__name__)
                     "function does not accept keyword arguments.")
         if has_self:
             args = args[1:]
-        return [namespace + "|" + key for key in map(compat.text_type, args)]
+        return [namespace + "|" + key for key in map(to_str, args)]
     return generate_keys
 
 def sha1_mangle_key(key):

tests/cache/__init__.py

 import re
-
+from nose import SkipTest
+from functools import wraps
+from dogpile.cache import compat
 
 def eq_(a, b, msg=None):
     """Assert a == b, with repr messaging on failure."""
         assert re.search(msg, str(e)), "%r !~ %s" % (msg, e)
 
 from dogpile.cache.compat import configparser, io
+
+
+def requires_py3k(fn):
+    @wraps(fn)
+    def wrap(*arg, **kw):
+        if compat.py2k:
+            raise SkipTest("Python 3 required")
+        return fn(*arg, **kw)
+    return wrap

tests/cache/test_decorator.py

 #! coding: utf-8
 
 from ._fixtures import _GenericBackendFixture
-from . import eq_
+from . import eq_, requires_py3k
 from unittest import TestCase
 import time
 from dogpile.cache import util, compat
         eq_(go(1, 2), ['2 1', '2 2'])
 
 class KeyGenerationTest(TestCase):
-    def _keygen_decorator(self, namespace=None):
+    def _keygen_decorator(self, namespace=None, **kw):
         canary = []
         def decorate(fn):
-            canary.append(util.function_key_generator(namespace, fn))
+            canary.append(util.function_key_generator(namespace, fn, **kw))
             return fn
         return decorate, canary
 
-    def _multi_keygen_decorator(self, namespace=None):
+    def _multi_keygen_decorator(self, namespace=None, **kw):
         canary = []
         def decorate(fn):
-            canary.append(util.function_multi_key_generator(namespace, fn))
+            canary.append(util.function_multi_key_generator(namespace, fn, **kw))
             return fn
         return decorate, canary
 
         eq_(gen(1, 2), "tests.cache.test_decorator:one|mynamespace|1 2")
         eq_(gen(None, 5), "tests.cache.test_decorator:one|mynamespace|None 5")
 
-    def test_unicode_key(self):
+    def test_key_isnt_unicode_bydefault(self):
         decorate, canary = self._keygen_decorator("mynamespace")
 
         @decorate
             pass
         gen = canary[0]
 
+        assert isinstance(gen('foo'), str)
+
+    def test_unicode_key(self):
+        decorate, canary = self._keygen_decorator("mynamespace",
+                                        to_str=compat.text_type)
+
+        @decorate
+        def one(a, b):
+            pass
+        gen = canary[0]
+
         eq_(gen(compat.u('méil'), compat.u('drôle')),
                 compat.ue("tests.cache.test_decorator:"
                         "one|mynamespace|m\xe9il dr\xf4le"))
 
     def test_unicode_key_multi(self):
-        decorate, canary = self._multi_keygen_decorator("mynamespace")
+        decorate, canary = self._multi_keygen_decorator("mynamespace",
+                                        to_str=compat.text_type)
 
         @decorate
         def one(a, b):
                 compat.ue('tests.cache.test_decorator:one|mynamespace|dr\xf4le')
             ])
 
+    @requires_py3k
+    def test_unicode_key_by_default(self):
+        decorate, canary = self._keygen_decorator("mynamespace",
+                                        to_str=compat.text_type)
+
+        @decorate
+        def one(a, b):
+            pass
+        gen = canary[0]
+
+        assert isinstance(gen('méil'), str)
+
+        eq_(gen('méil', 'drôle'),
+                "tests.cache.test_decorator:"
+                        "one|mynamespace|m\xe9il dr\xf4le")
 
 class CacheDecoratorTest(_GenericBackendFixture, TestCase):
     backend = "mock"