Repository: aio-libs-abandoned/aioredis-py

Issue:
Transactions (multi_exec) do not honor encoding parameter
``` python
>>> __import__('sys').version
>>> loop = asyncio.get_event_loop()
'3.4.3 (default, Oct 30 2015, 20:31:57) \n[GCC 5.2.0]
>>> redis = aioredis.create_redis(('localhost', 6379), db=1, encoding='utf8', loop=loop)
>>> redis = loop.run_until_complete(redis)
>>> loop.run_until_complete(redis.lrange('test', 0, -1))
['ab', 'cd']  # strings
>>> tr = redis.multi_exec()
>>> tr.lrange('test', 0, -1)
>>> loop.run_until_complete(tr.execute())[0]
[b'ab', b'cd']  # bytes!

>>> tr = redis.multi_exec()
>>> tr.lrange('test', 0, -1, encoding='utf8')  # note the encoding
>>> loop.run_until_complete(tr.execute())[0]
[b'ab', b'cd']  # bytes!
```



Discussion:
ok, I will look into


Resolving patch:
diff --git a/aioredis/connection.py b/aioredis/connection.py
--- a/aioredis/connection.py
+++ b/aioredis/connection.py
@@ -86,8 +86,8 @@ def __init__(self, reader, writer, *, encoding=None, loop=None):
         self._closed = False
         self._close_waiter = asyncio.Future(loop=self._loop)
         self._reader_task.add_done_callback(self._close_waiter.set_result)
-        self._in_transaction = False
-        self._transaction_error = None
+        self._in_transaction = None
+        self._transaction_error = None  # XXX: never used?
         self._in_pubsub = 0
         self._pubsub_channels = coerced_keys_dict()
         self._pubsub_patterns = coerced_keys_dict()
@@ -118,7 +118,7 @@ def _read_data(self):
                     # so connection must be closed
                     self._closing = True
                     self._loop.call_soon(self._do_close, exc)
-                    if self._in_transaction:
+                    if self._in_transaction is not None:
                         self._transaction_error = exc
                     return
                 else:
@@ -141,7 +141,7 @@ def _process_data(self, obj):
             return
         if isinstance(obj, RedisError):
             waiter.set_exception(obj)
-            if self._in_transaction:
+            if self._in_transaction is not None:
                 self._transaction_error = obj
         else:
             if encoding is not None:
@@ -150,9 +150,15 @@ def _process_data(self, obj):
                 except Exception as exc:
                     waiter.set_exception(exc)
                     return
-            waiter.set_result(obj)
             if cb is not None:
-                cb(obj)
+                try:
+                    obj = cb(obj)
+                except Exception as exc:
+                    waiter.set_exception(exc)
+                    return
+            waiter.set_result(obj)
+            if self._in_transaction is not None:
+                self._in_transaction.append((encoding, cb))
 
     def _process_pubsub(self, obj, *, _process_waiters=True):
         """Processes pubsub messages."""
@@ -213,8 +219,10 @@ def execute(self, command, *args, encoding=_NOTSET):
             cb = partial(self._set_db, args=args)
         elif command in ('MULTI', b'MULTI'):
             cb = self._start_transaction
-        elif command in ('EXEC', b'EXEC', 'DISCARD', b'DISCARD'):
-            cb = self._end_transaction
+        elif command in ('EXEC', b'EXEC'):
+            cb = partial(self._end_transaction, discard=False)
+        elif command in ('DISCARD', b'DISCARD'):
+            cb = partial(self._end_transaction, discard=True)
         else:
             cb = None
         if encoding is _NOTSET:
@@ -312,27 +320,47 @@ def select(self, db):
     def _set_db(self, ok, args):
         assert ok in {b'OK', 'OK'}, ok
         self._db = args[0]
+        return ok
 
     def _start_transaction(self, ok):
-        assert not self._in_transaction
-        self._in_transaction = True
+        assert self._in_transaction is None
+        self._in_transaction = deque()
         self._transaction_error = None
+        return ok
 
-    def _end_transaction(self, ok):
-        assert self._in_transaction
-        self._in_transaction = False
+    def _end_transaction(self, obj, discard):
+        assert self._in_transaction is not None
         self._transaction_error = None
+        recall, self._in_transaction = self._in_transaction, None
+        recall.popleft()  # ignore first (its _start_transaction)
+        if discard:
+            return obj
+        assert len(obj) == len(recall), (obj, recall)
+        res = []
+        for o, (encoding, cb) in zip(obj, recall):
+            if not isinstance(o, RedisError):
+                try:
+                    if encoding:
+                        o = decode(o, encoding)
+                    if cb:
+                        o = cb(o)
+                except Exception as err:
+                    res.append(err)
+                    continue
+            res.append(o)
+        return res
 
     def _update_pubsub(self, obj):
         *head, subscriptions = obj
         self._in_pubsub, was_in_pubsub = subscriptions, self._in_pubsub
         if not was_in_pubsub:
             self._process_pubsub(obj, _process_waiters=False)
+        return obj
 
     @property
     def in_transaction(self):
         """Set to True when MULTI command was issued."""
-        return self._in_transaction
+        return self._in_transaction is not None
 
     @property
     def in_pubsub(self):


Test patch:
diff --git a/tests/string_commands_test.py b/tests/string_commands_test.py
--- a/tests/string_commands_test.py
+++ b/tests/string_commands_test.py
@@ -642,4 +642,5 @@ def test_setnx(self):
         res = yield from self.redis.setnx(TEST_KEY, 'value')
         self.assertEqual(res, 'QUEUED')
 
-        yield from self.redis._conn.execute('DISCARD')
+        ok = yield from self.redis._conn.execute('DISCARD')
+        self.assertEqual(ok, 'OK')
diff --git a/tests/transaction_commands_test.py b/tests/transaction_commands_test.py
--- a/tests/transaction_commands_test.py
+++ b/tests/transaction_commands_test.py
@@ -158,3 +158,46 @@ def test_watch_unwatch(self):
 
         res = yield from self.redis.unwatch()
         self.assertTrue(res)
+
+    @run_until_complete
+    def test_encoding(self):
+        res = yield from self.redis.set('key', 'value')
+        self.assertTrue(res)
+        res = yield from self.redis.hmset(
+            'hash-key', 'foo', 'val1', 'bar', 'val2')
+        self.assertTrue(res)
+
+        tr = self.redis.multi_exec()
+        fut1 = tr.get('key')
+        fut2 = tr.get('key', encoding='utf-8')
+        fut3 = tr.hgetall('hash-key', encoding='utf-8')
+        yield from tr.execute()
+        res = yield from fut1
+        self.assertEqual(res, b'value')
+        res = yield from fut2
+        self.assertEqual(res, 'value')
+        res = yield from fut3
+        self.assertEqual(res, {'foo': 'val1', 'bar': 'val2'})
+
+    @run_until_complete
+    def test_global_encoding(self):
+        redis = yield from self.create_redis(
+            ('localhost', self.redis_port),
+            loop=self.loop, encoding='utf-8')
+        res = yield from redis.set('key', 'value')
+        self.assertTrue(res)
+        res = yield from redis.hmset(
+            'hash-key', 'foo', 'val1', 'bar', 'val2')
+        self.assertTrue(res)
+
+        tr = redis.multi_exec()
+        fut1 = tr.get('key')
+        fut2 = tr.get('key', encoding='utf-8')
+        fut3 = tr.hgetall('hash-key', encoding='utf-8')
+        yield from tr.execute()
+        res = yield from fut1
+        self.assertEqual(res, 'value')
+        res = yield from fut2
+        self.assertEqual(res, 'value')
+        res = yield from fut3
+        self.assertEqual(res, {'foo': 'val1', 'bar': 'val2'})