Repository: redis/redis-py

Issue:
xreadgroup throws an exception if message was trimmed when reading pending messages
**Version**: 3.0.1

**Platform**: Python 3.6.6 on Debian 9 and Redis 5.0.2

**Description**: If messages get deleted from a STREAM via XADD with MAXLEN or XTRIM in a stream where there is an existing consumer group after it's consumed but before being acked, when you consume the pending messages using XREADGROUP with id 0, redis returns a (nil) as the payload of the messages instead of field key/value pairs. This causes xreadgroup to throw an exception:
```
[2019-01-10 12:00:10,206] [16] [ERROR] Error reading messages
Traceback (most recent call last):
  File "/code/app/daemons/__init__.py", line 38, in run
    stream_messages = self.redis_client.xreadgroup("consumer-group", "consumer", {"stream": "0"}, count=1000)
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 2185, in xreadgroup
    return self.execute_command('XREADGROUP', *pieces)
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 755, in execute_command
    return self.parse_response(connection, command_name, **options)
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 774, in parse_response
    return self.response_callbacks[command_name](response, **options)
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 275, in parse_xread
    return [[nativestr(r[0]), parse_stream_list(r[1])] for r in response]
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 275, in <listcomp>
    return [[nativestr(r[0]), parse_stream_list(r[1])] for r in response]
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 246, in parse_stream_list
    return [(r[0], pairs_to_dict(r[1])) for r in response]
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 246, in <listcomp>
    return [(r[0], pairs_to_dict(r[1])) for r in response]
  File "/usr/local/lib/python3.6/site-packages/redis/client.py", line 195, in pairs_to_dict
    it = iter(response)
TypeError: 'NoneType' object is not iterable
```

You can see this is the expected behaviour in Redis's source code:
https://github.com/antirez/redis/blob/5.0.2/src/t_stream.c#L1031





Discussion:

Resolving patch:
diff --git a/redis/client.py b/redis/client.py
--- a/redis/client.py
+++ b/redis/client.py
@@ -191,6 +191,8 @@ def parse_sentinel_get_master(response):
 
 def pairs_to_dict(response, decode_keys=False):
     "Create a dict given a list of key/value pairs"
+    if response is None:
+        return {}
     if decode_keys:
         # the iter form is faster, but I don't know how to make that work
         # with a nativestr() map


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -2205,15 +2205,30 @@ def test_xreadgroup(self, r):
         # xread starting after the last message returns an empty message list
         assert r.xreadgroup(group, consumer, streams={stream: '>'}) == expected
 
-        r.xgroup_destroy(stream, group)
-
         # xreadgroup with noack does not have any items in the PEL
+        r.xgroup_destroy(stream, group)
         r.xgroup_create(stream, group, '0')
         assert len(r.xreadgroup(group, consumer, streams={stream: '>'},
                                 noack=True)[0][1]) == 2
         # now there should be nothing pending
         assert len(r.xreadgroup(group, consumer,
                                 streams={stream: '0'})[0][1]) == 0
+        
+        r.xgroup_destroy(stream, group)
+        r.xgroup_create(stream, group, '0')
+        # delete all the messages in the stream
+        expected = [
+            [
+                stream.encode(),
+                [
+                    (m1, {}),
+                    (m2, {}),
+                ]
+            ]
+        ]
+        r.xreadgroup(group, consumer, streams={stream: '>'})
+        r.xtrim(stream, 0)
+        assert r.xreadgroup(group, consumer, streams={stream: '0'}) == expected
 
     @skip_if_server_version_lt('5.0.0')
     def test_xrevrange(self, r):