Repository: aio-libs-abandoned/aioredis-py

Issue:
aioredis.pubsub.Receiver has no iter() method
The example code (below) references an iter() method on Receiver (here is the [implementation](https://github.com/aio-libs/aioredis/blob/master/aioredis/pubsub.py#L200)).

There is no iter() method on Receiver. What is the correct implementation of listening on all Receiver's subscribed channels?

code:
```
>>> from aioredis.pubsub import Receiver
>>> from aioredis.abc import AbcChannel
>>> mpsc = Receiver(loop=loop)
>>> async def reader(mpsc):
...     async for channel, msg in mpsc.iter():
...         assert isinstance(channel, AbcChannel)
...         print("Got {!r} in channel {!r}".format(msg, channel))
>>> asyncio.ensure_future(reader(mpsc))
>>> await redis.subscribe(mpsc.channel('channel:1'),
...                       mpsc.channel('channel:3'))
...                       mpsc.channel('channel:5'))
>>> await redis.psubscribe(mpsc.pattern('hello'))
>>> # publishing 'Hello world' into 'hello-channel'
>>> # will print this message:
Got b'Hello world' in channel b'hello-channel'
>>> # when all is done:
>>> await redis.unsubscribe('channel:1', 'channel:3', 'channel:5')
>>> await redis.punsubscribe('hello')
>>> mpsc.stop()
>>> # any message received after stop() will be ignored.
```


Discussion:
This implementation does work:
```
class IterableReceiver(Receiver):
    def __aiter__(self):
        return self

    async def __anext__(self):
        await self.wait_message()
        return await self.get()


mpsc = IterableReceiver(loop=loop)

async def reader(mpsc):
    async for channel, msg in mpsc:
        assert isinstance(channel, AbcChannel)
        print("Got {!r} in channel {!r}".format(msg, channel))
asyncio.ensure_future(reader(mpsc))
```
Thanks for report.
Fixed in v0.3 branch, will make a bug-fix release soon.
Out of curiosity, when do you intend to publish an update that addresses this? It looks like the last update was in Jan. https://pypi.org/project/aioredis/
OMG, I thought it was like a month ago...
I think I can do a release in a day or two.

Resolving patch:
diff --git a/aioredis/commands/__init__.py b/aioredis/commands/__init__.py
--- a/aioredis/commands/__init__.py
+++ b/aioredis/commands/__init__.py
@@ -1,9 +1,10 @@
 import asyncio
-import warnings
+# import warnings
 
 from aioredis.connection import create_connection
 from aioredis.pool import create_pool
 from aioredis.util import _NOTSET
+from aioredis.abc import AbcPool
 from .generic import GenericCommandsMixin
 from .string import StringCommandsMixin
 from .hash import HashCommandsMixin
@@ -46,7 +47,7 @@ def __init__(self, pool_or_conn):
         self._pool_or_conn = pool_or_conn
 
     def __repr__(self):
-        return '<Redis {!r}>'.format(self._pool_or_conn)
+        return '<{} {!r}>'.format(self.__class__.__name__, self._pool_or_conn)
 
     def execute(self, command, *args, **kwargs):
         return self._pool_or_conn.execute(command, *args, **kwargs)
@@ -127,18 +128,38 @@ def select(self, db):
         """
         return self._pool_or_conn.select(db)
 
+    def __await__(self):
+        if isinstance(self._pool_or_conn, AbcPool):
+            conn = yield from self._pool_or_conn.acquire()
+            release = self._pool_or_conn.release
+        else:
+            # TODO: probably a lock is needed here if _pool_or_conn
+            #       is Connection instance.
+            conn = self._pool_or_conn
+            release = None
+        return ContextRedis(conn, release)
+    __iter__ = __await__
+
+
+class ContextRedis(Redis):
+    """An instance of Redis class bound to single connection."""
+
+    def __init__(self, conn, release_cb=None):
+        super().__init__(conn)
+        self._release_callback = release_cb
+
     def __enter__(self):
-        # TODO: warn it is obsolete way
-        warnings.warn("It is not recommended way to use Redis instance"
-                      " as a context manager. Use Redis.<command> directly")
         return self
 
-    def __exit__(self, *args):
-        pass
+    def __exit__(self, *exc_info):
+        if self._release_callback is not None:
+            conn, self._pool_or_conn = self._pool_or_conn, None
+            self._release_callback(conn)
 
-    def __iter__(self):
-        return self
+    def __await__(self):
+        return ContextRedis(self._pool_or_conn)
         yield
+    __iter__ = __await__
 
 
 @asyncio.coroutine


Test patch:
diff --git a/tests/connection_commands_test.py b/tests/connection_commands_test.py
--- a/tests/connection_commands_test.py
+++ b/tests/connection_commands_test.py
@@ -91,8 +91,9 @@ def test_yield_from_backwards_compatability(create_redis, server, loop):
     redis = yield from create_redis(server.tcp_address, loop=loop)
 
     assert isinstance(redis, Redis)
-    with pytest.warns(UserWarning):
-        with (yield from redis) as client:
-            assert isinstance(client, Redis)
-            assert client is redis
-            assert (yield from client.ping())
+    # TODO: there should not be warning
+    # with pytest.warns(UserWarning):
+    with (yield from redis) as client:
+        assert isinstance(client, Redis)
+        assert client is not redis
+        assert (yield from client.ping())
diff --git a/tests/integrational_test.py b/tests/integrational_test.py
new file mode 100644
--- /dev/null
+++ b/tests/integrational_test.py
@@ -0,0 +1,105 @@
+import asyncio
+import pytest
+
+import aioredis
+from aioredis.util import async_task
+
+
+@pytest.fixture
+def pool_or_redis(_closable, server, loop):
+    version = tuple(map(int, aioredis.__version__.split('.')[:2]))
+    if version >= (1, 0):
+        factory = aioredis.create_redis_pool
+    else:
+        factory = aioredis.create_pool
+
+    @asyncio.coroutine
+    def redis_factory(maxsize):
+        redis = yield from factory(server.tcp_address, loop=loop,
+                                   minsize=1, maxsize=maxsize)
+        _closable(redis)
+        return redis
+    return redis_factory
+
+
+@asyncio.coroutine
+def simple_get_set(pool, idx, loop):
+    """A simple test to make sure Redis(pool) can be used as old Pool(Redis).
+    """
+    val = 'val:{}'.format(idx)
+    with (yield from pool) as redis:
+        assert (yield from redis.set('key', val))
+        yield from redis.get('key', encoding='utf-8')
+
+
+@asyncio.coroutine
+def pipeline(pool, val, loop):
+    val = 'val:{}'.format(val)
+    with (yield from pool) as redis:
+        f1 = redis.set('key', val)
+        f2 = redis.get('key', encoding='utf-8')
+        ok, res = yield from asyncio.gather(f1, f2, loop=loop)
+
+
+@asyncio.coroutine
+def transaction(pool, val, loop):
+    val = 'val:{}'.format(val)
+    with (yield from pool) as redis:
+        tr = redis.multi_exec()
+        tr.set('key', val)
+        tr.get('key', encoding='utf-8')
+        ok, res = yield from tr.execute()
+        assert ok, ok
+        assert res == val
+
+
+@asyncio.coroutine
+def blocking_pop(pool, val, loop):
+
+    @asyncio.coroutine
+    def lpush():
+        with (yield from pool) as redis:
+            # here v0.3 has bound connection, v1.0 does not;
+            yield from asyncio.sleep(.1, loop=loop)
+            yield from redis.lpush('list-key', 'val')
+
+    @asyncio.coroutine
+    def blpop():
+        with (yield from pool) as redis:
+            # here v0.3 has bound connection, v1.0 does not;
+            res = yield from redis.blpop(
+                'list-key', timeout=1, encoding='utf-8')
+            assert res == ['list-key', 'val'], res
+    yield from asyncio.gather(blpop(), lpush(), loop=loop)
+
+
+@pytest.mark.run_loop
+@pytest.mark.parametrize('test_case,pool_size', [
+    (simple_get_set, 1),
+    (pipeline, 1),
+    (transaction, 1),
+    pytest.mark.xfail((blocking_pop, 1),
+                      reason="blpop gets connection first and blocks"),
+    (simple_get_set, 10),
+    (pipeline, 10),
+    (transaction, 10),
+    (blocking_pop, 10),
+], ids=lambda o: o.__name__)
+def test_operations(pool_or_redis, test_case, pool_size, loop):
+    repeat = 100
+    redis = yield from pool_or_redis(pool_size)
+    done, pending = yield from asyncio.wait(
+        [async_task(test_case(redis, i, loop), loop=loop)
+         for i in range(repeat)], loop=loop)
+
+    assert not pending
+    success = 0
+    failures = []
+    for fut in done:
+        exc = fut.exception()
+        if exc is None:
+            success += 1
+        else:
+            failures.append(exc)
+    assert repeat == success, failures
+    assert not failures