Repository: pallets/werkzeug

Issue:
Werkzeug-0.9.4 and python3_memcached-1.51 doesn't work on Python 3.4
I'm using `werkzeug.contrib.cache.MemcachedCache` with python3_memcached-1.51 when I try to get\set data I get the following error: `memcache.MemcachedKeyTypeError: Key must be str()'s` here:
https://github.com/eguven/python3-memcached/blob/master/memcache.py#L1039

```
if not isinstance(key, str):
    raise Client.MemcachedKeyTypeError("Key must be str()'s")
```

The key type is `bytes`, not `str`. So I checked deeper.

Inside `werkzeug.contrib.cache.MemcachedCache` key is encoded into `bytes` (in Python 3) like this:

```
if isinstance(key, text_type):
    key = key.encode('utf-8')
```

https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/contrib/cache.py#L342

What is correct fix for this? Monkey patch?



Discussion:
I encountered the same problem and had to monkey patch a bit. I am not sure what is the official status on the Python 3 support (I feel like it is discouraged and Python 2 is preferred for some reason) but I see no simple solution without introducing conditional statements in the code. I am surprised however why there is no separate function preparing keys before using them which would make things easier to patch. I am not sure if that can be changed or if it is a deliberate design decision.


Resolving patch:
diff --git a/werkzeug/contrib/cache.py b/werkzeug/contrib/cache.py
--- a/werkzeug/contrib/cache.py
+++ b/werkzeug/contrib/cache.py
@@ -68,7 +68,7 @@ def get_sidebar(user):
     import pickle
 
 from werkzeug._compat import iteritems, string_types, text_type, \
-     integer_types, to_bytes
+    integer_types, to_native
 from werkzeug.posixemulation import rename
 
 
@@ -294,7 +294,7 @@ def delete(self, key):
         return self._cache.pop(key, None) is not None
 
 
-_test_memcached_key = re.compile(br'[^\x00-\x21\xff]{1,250}$').match
+_test_memcached_key = re.compile(r'[^\x00-\x21\xff]{1,250}$').match
 
 class MemcachedCache(BaseCache):
     """A cache that uses memcached as backend.
@@ -335,13 +335,16 @@ def __init__(self, servers=None, default_timeout=300, key_prefix=None):
             # client.
             self._client = servers
 
-        self.key_prefix = to_bytes(key_prefix)
+        self.key_prefix = to_native(key_prefix)
 
-    def get(self, key):
-        if isinstance(key, text_type):
-            key = key.encode('utf-8')
+    def _normalize_key(self, key):
+        key = to_native(key, 'utf-8')
         if self.key_prefix:
             key = self.key_prefix + key
+        return key
+
+    def get(self, key):
+        key = self._normalize_key(key)
         # memcached doesn't support keys longer than that.  Because often
         # checks for so long keys can occour because it's tested from user
         # submitted data etc we fail silently for getting.
@@ -352,13 +355,9 @@ def get_dict(self, *keys):
         key_mapping = {}
         have_encoded_keys = False
         for key in keys:
-            if isinstance(key, unicode):
-                encoded_key = key.encode('utf-8')
+            encoded_key = self._normalize_key(key)
+            if not isinstance(key, str):
                 have_encoded_keys = True
-            else:
-                encoded_key = key
-            if self.key_prefix:
-                encoded_key = self.key_prefix + encoded_key
             if _test_memcached_key(key):
                 key_mapping[encoded_key] = key
         d = rv = self._client.get_multi(key_mapping.keys())
@@ -375,19 +374,13 @@ def get_dict(self, *keys):
     def add(self, key, value, timeout=None):
         if timeout is None:
             timeout = self.default_timeout
-        if isinstance(key, text_type):
-            key = key.encode('utf-8')
-        if self.key_prefix:
-            key = self.key_prefix + key
+        key = self._normalize_key(key)
         return self._client.add(key, value, timeout)
 
     def set(self, key, value, timeout=None):
         if timeout is None:
             timeout = self.default_timeout
-        if isinstance(key, text_type):
-            key = key.encode('utf-8')
-        if self.key_prefix:
-            key = self.key_prefix + key
+        key = self._normalize_key(key)
         return self._client.set(key, value, timeout)
 
     def get_many(self, *keys):
@@ -399,29 +392,20 @@ def set_many(self, mapping, timeout=None):
             timeout = self.default_timeout
         new_mapping = {}
         for key, value in _items(mapping):
-            if isinstance(key, text_type):
-                key = key.encode('utf-8')
-            if self.key_prefix:
-                key = self.key_prefix + key
+            key = self._normalize_key(key)
             new_mapping[key] = value
         failed_keys = self._client.set_multi(new_mapping, timeout)
         return not failed_keys
 
     def delete(self, key):
-        if isinstance(key, unicode):
-            key = key.encode('utf-8')
-        if self.key_prefix:
-            key = self.key_prefix + key
+        key = self._normalize_key(key)
         if _test_memcached_key(key):
             return self._client.delete(key)
 
     def delete_many(self, *keys):
         new_keys = []
         for key in keys:
-            if isinstance(key, unicode):
-                key = key.encode('utf-8')
-            if self.key_prefix:
-                key = self.key_prefix + key
+            key = self._normalize_key(key)
             if _test_memcached_key(key):
                 new_keys.append(key)
         return self._client.delete_multi(new_keys)
@@ -430,17 +414,11 @@ def clear(self):
         return self._client.flush_all()
 
     def inc(self, key, delta=1):
-        if isinstance(key, unicode):
-            key = key.encode('utf-8')
-        if self.key_prefix:
-            key = self.key_prefix + key
+        key = self._normalize_key(key)
         return self._client.incr(key, delta)
 
     def dec(self, key, delta=1):
-        if isinstance(key, unicode):
-            key = key.encode('utf-8')
-        if self.key_prefix:
-            key = self.key_prefix + key
+        key = self._normalize_key(key)
         return self._client.decr(key, delta)
 
     def import_preferred_memcache_lib(self, servers):


Test patch:
diff --git a/tests/contrib/test_cache.py b/tests/contrib/test_cache.py
--- a/tests/contrib/test_cache.py
+++ b/tests/contrib/test_cache.py
@@ -183,5 +183,5 @@ def preparefunc(cwd):
             return lambda: c
 
         def test_compat(self, c):
-            assert c._client.set(c.key_prefix + b'foo', 'bar')
+            assert c._client.set(c.key_prefix + 'foo', 'bar')
             assert c.get('foo') == 'bar'