Repository: aio-libs-abandoned/aioredis-py

Issue:
Can't pass bytearray as value to SET
Given that `bytes` can be used as arguments to Redis commands, it seems reasonable to also support `bytearray`.

This is currently impossible due to a check for `None` in the args to `RedisConnection::execute` which involves the creation of a set from said args. While I understand this optimization, it precludes the use of `bytearray` as it is not hashable (see below).

No Redis command accepts more than 10 or so args; just scanning the list without creating a set feels like an acceptable tradeoff.

## Example

Here's a snippet from an IPython session demonstrating the issue:

```
In [6]: data
Out[6]: bytearray(b'hello world')

In [7]: result = loop.run_until_complete(redis.set('some-sweet-key', data, expire=86400))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-7f1e0c5ed0b8> in <module>()
----> 1 result = loop.run_until_complete(redis.set('some-sweet-key', data, expire=86400))

/usr/local/lib/python3.6/site-packages/aioredis/commands/string.py in set(self, key, value, expire, pexpire, exist)
    184         elif exist is self.SET_IF_NOT_EXIST:
    185             args.append(b'NX')
--> 186         fut = self.execute(b'SET', key, value, *args)
    187         return wait_ok(fut)
    188 

/usr/local/lib/python3.6/site-packages/aioredis/commands/__init__.py in execute(self, command, *args, **kwargs)
     50 
     51     def execute(self, command, *args, **kwargs):
---> 52         return self._pool_or_conn.execute(command, *args, **kwargs)
     53 
     54     def close(self):

/usr/local/lib/python3.6/site-packages/aioredis/pool.py in execute(self, command, *args, **kw)
    200         conn, address = self.get_connection(command, args)
    201         if conn is not None:
--> 202             fut = conn.execute(command, *args, **kw)
    203             return self._check_result(fut, command, args, kw)
    204         else:

/usr/local/lib/python3.6/site-packages/aioredis/connection.py in execute(self, command, encoding, *args)
    261         if command is None:
    262             raise TypeError("command must not be None")
--> 263         if None in set(args):
    264             raise TypeError("args must not contain None")
    265         command = command.upper().strip()

TypeError: unhashable type: 'bytearray'
```

## Environment

* Python 3.6
* aioredis 1.0.0b2


Discussion:
Thanks for the report,
This is a simple bug, would you mind creating a Pull Request with a patch and tests?

Resolving patch:
diff --git a/aioredis/connection.py b/aioredis/connection.py
--- a/aioredis/connection.py
+++ b/aioredis/connection.py
@@ -287,7 +287,7 @@ def execute(self, command, *args, encoding=_NOTSET):
             raise ConnectionClosedError(msg)
         if command is None:
             raise TypeError("command must not be None")
-        if None in set(args):
+        if None in args:
             raise TypeError("args must not contain None")
         command = command.upper().strip()
         is_pubsub = command in _PUBSUB_COMMANDS
diff --git a/aioredis/util.py b/aioredis/util.py
--- a/aioredis/util.py
+++ b/aioredis/util.py
@@ -13,7 +13,7 @@
     str: lambda val: val.encode('utf-8'),
     int: lambda val: str(val).encode('utf-8'),
     float: lambda val: str(val).encode('utf-8'),
-    }
+}
 
 
 def _bytes_len(sized):
@@ -23,7 +23,8 @@ def _bytes_len(sized):
 def encode_command(*args):
     """Encodes arguments into redis bulk-strings array.
 
-    Raises TypeError if any of args not of bytes, str, int or float type.
+    Raises TypeError if any of args not of bytearray, bytes, float, int, or str
+    type.
     """
     buf = bytearray()
 
@@ -37,8 +38,8 @@ def add(data):
             add(b'$' + _bytes_len(barg))
             add(barg)
         else:
-            raise TypeError("Argument {!r} expected to be of bytes,"
-                            " str, int or float type".format(arg))
+            raise TypeError("Argument {!r} expected to be of bytearray, bytes,"
+                            " float, int, or str type".format(arg))
     return buf
 
 


Test patch:
diff --git a/tests/encode_command_test.py b/tests/encode_command_test.py
--- a/tests/encode_command_test.py
+++ b/tests/encode_command_test.py
@@ -17,6 +17,14 @@ def test_encode_bytes():
     assert res == b'*1\r\n$7\r\nHello\r\n\r\n'
 
 
+def test_encode_bytearray():
+    res = encode_command(bytearray(b'Hello'))
+    assert res == b'*1\r\n$5\r\nHello\r\n'
+
+    res = encode_command(bytearray(b'Hello'), bytearray(b'world'))
+    assert res == b'*2\r\n$5\r\nHello\r\n$5\r\nworld\r\n'
+
+
 def test_encode_str():
     res = encode_command('Hello')
     assert res == b'*1\r\n$5\r\nHello\r\n'
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
@@ -481,6 +481,12 @@ async def test_set(redis):
     ok = await redis.set('my-key', 'value')
     assert ok is True
 
+    ok = await redis.set(b'my-key', b'value')
+    assert ok is True
+
+    ok = await redis.set(bytearray(b'my-key'), bytearray(b'value'))
+    assert ok is True
+
     with pytest.raises(TypeError):
         await redis.set(None, 'value')