Repository: aio-libs-abandoned/aioredis-py

Issue:
`HashCommandsMixin.hset()` doesn't support multiple field=value pairs
> As of Redis 4.0.0, HSET is variadic and allows for multiple field/value pairs.

https://redis.io/commands/hset

And also info about HMSET usage:
> As per Redis 4.0.0, HMSET is considered deprecated. Please use HSET in new code.

https://redis.io/commands/hmset


Discussion:

Resolving patch:
diff --git a/aioredis/commands/hash.py b/aioredis/commands/hash.py
--- a/aioredis/commands/hash.py
+++ b/aioredis/commands/hash.py
@@ -1,3 +1,4 @@
+import warnings
 from itertools import chain
 
 from aioredis.util import (
@@ -55,7 +56,17 @@ def hmget(self, key, field, *fields, encoding=_NOTSET):
         return self.execute(b"HMGET", key, field, *fields, encoding=encoding)
 
     def hmset(self, key, field, value, *pairs):
-        """Set multiple hash fields to multiple values."""
+        """Set multiple hash fields to multiple values.
+
+        .. deprecated::
+            HMSET is deprecated since redis 4.0.0, use HSET instead.
+
+        """
+        warnings.warn(
+            "%s.hmset() is deprecated since redis 4.0.0, use %s.hset() instead"
+            % (self.__class__.__name__, self.__class__.__name__),
+            DeprecationWarning
+        )
         if len(pairs) % 2 != 0:
             raise TypeError("length of pairs must be even number")
         return wait_ok(self.execute(b"HMSET", key, field, value, *pairs))
@@ -63,6 +74,9 @@ def hmset(self, key, field, value, *pairs):
     def hmset_dict(self, key, *args, **kwargs):
         """Set multiple hash fields to multiple values.
 
+        .. deprecated::
+            HMSET is deprecated since redis 4.0.0, use HSET instead.
+
         dict can be passed as first positional argument:
 
         >>> await redis.hmset_dict(
@@ -86,6 +100,12 @@ def hmset_dict(self, key, *args, **kwargs):
            'baz'
 
         """
+        warnings.warn(
+            "%s.hmset() is deprecated since redis 4.0.0, use %s.hset() instead"
+            % (self.__class__.__name__, self.__class__.__name__),
+            DeprecationWarning
+        )
+
         if not args and not kwargs:
             raise TypeError("args or kwargs must be specified")
         pairs = ()
@@ -100,9 +120,31 @@ def hmset_dict(self, key, *args, **kwargs):
         kwargs_pairs = chain.from_iterable(kwargs.items())
         return wait_ok(self.execute(b"HMSET", key, *chain(pairs, kwargs_pairs)))
 
-    def hset(self, key, field, value):
-        """Set the string value of a hash field."""
-        return self.execute(b"HSET", key, field, value)
+    def hset(self, key, field=None, value=None, mapping=None):
+        """Set multiple hash fields to multiple values.
+
+        Setting a single hash field to a value:
+        >>> await redis.hset('key', 'some_field', 'some_value')
+
+        Setting values for multipe has fields at once:
+        >>> await redis.hset('key', mapping={'field1': 'abc', 'field2': 'def'})
+
+        .. note:: Using both the field/value pair and mapping at the same time
+           will also work.
+
+        """
+        if not field and not mapping:
+            raise ValueError("hset needs either a field/value pair or mapping")
+        if mapping and not isinstance(mapping, dict):
+            raise TypeError("'mapping' should be dict")
+
+        items = []
+        if field:
+            items.extend((field, value))
+        if mapping:
+            for item in mapping.items():
+                items.extend(item)
+        return self.execute(b"HSET", key, *items)
 
     def hsetnx(self, key, field, value):
         """Set the value of a hash field, only if the field does not exist."""


Test patch:
diff --git a/tests/hash_commands_test.py b/tests/hash_commands_test.py
--- a/tests/hash_commands_test.py
+++ b/tests/hash_commands_test.py
@@ -317,6 +317,44 @@ async def test_hset(redis):
         await redis.hset(None, field, value)
 
 
+@redis_version(4, 0, 0, reason="HSET changed to variadic in redis 4.0.0")
+@pytest.mark.asyncio
+async def test_hset_multiple(redis):
+    test_key = b"key:hset_multiple"
+
+    #  raises ValueError if field/value pair and mapping both are missing
+    with pytest.raises(ValueError):
+        await redis.hset(test_key)
+
+    #  raises TypeError if mapping is not a dict
+    with pytest.raises(TypeError):
+        await redis.hset(test_key, mapping="not a dict")
+
+    #  test multiple fields and string values
+    mapping = {b"a": b"1", b"b": b"2", b"c": b"test1", b"d": b"test string"}
+    #  insert mapping through hset
+    test_value = await redis.hset(test_key, mapping=mapping)
+    #  check if 4 values were added
+    assert test_value == 4
+
+    for field, value in mapping.items():
+        #  check if each field exists in the hash
+        result = await redis.hexists(test_key, field)
+        assert result == True
+
+        #  check value of each field matches the correct value
+        test_value = await redis.hget(test_key, field)
+        assert test_value == value
+
+    #  test imput of both field/value pair and mapping at same time
+    test_value = await redis.hset(test_key, "e", "5", mapping={"f": "6"})
+    assert test_value == 2
+    value1 = await redis.hget(test_key, "e")
+    value2 = await redis.hget(test_key, "f")
+    assert value1 == b"5"
+    assert value2 == b"6"
+
+
 @pytest.mark.asyncio
 async def test_hsetnx(redis):
     key, field, value = b"key:hsetnx", b"bar", b"zap"