Repository: aio-libs-abandoned/aioredis-py

Issue:
ZRANGEBYLEX does not handle min of "+" or max of "-"
The code for handling the special values `+` and `-` in zrangebylex only handles `-` for `min` and `+` for max, not vice versa. For example, the following code prints `[b'bar']` instead of an empty list:

```python
import asyncio
import aioredis

async def main():
    client = await aioredis.create_redis_pool('redis://localhost')
    await client.zadd('foo', 0.0, 'bar')
    result = await client.zrangebylex('foo', b'+', b'+')
    print(result)
    client.close()
    await client.wait_closed()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
```

This also affects `zrevrangebylex` and `zremrangebylex`.

Incidentally, the API design is problematic because it's impossible to pass strings like `(-` (meaning, strings lexicographically greater than `-`) into redis.


Discussion:
@bmerry -

Thanks for this submission! Would you be willing to contribute a potential fix to the API? Maintenance of this library is back on and the next release will be a breaking change, so updating the API to work as expected would be a great boon.
Yes, I may be able to contribute something, depending on the timeframe. When are you thinking of releasing the next version?
@seandstewart I can think of two possibilities for eliminating the ambiguity. I'm going to start on the first, but let me know if you'd rather I did the second.

1. Eliminate the `include_min` and `include_max` arguments, and make it the user's responsibiity to prepend a `[` or `(`. This will break almost all code that uses these APIs, but it will break them in a way that gives an error message. It will also make the implementation simpler as it will just be forwarding to redis.

2. Introduce singleton constants meaning "from the start" / "from the end", similar to `ZSET_AGGREGATE_SUM` etc; and treat `"-"` and `"+"` as literal strings which will get a prefix. This will keep a lot of code working as is (any code that doesn't explicitly pass a `-` or `+`), but code that breaks will break subtly (e.g. will start excluding anything that comes before "-" lexicographically.

Resolving patch:
diff --git a/aioredis/commands/sorted_set.py b/aioredis/commands/sorted_set.py
--- a/aioredis/commands/sorted_set.py
+++ b/aioredis/commands/sorted_set.py
@@ -141,21 +141,10 @@ def zinterstore(self, destkey, key, *keys, with_weights=False, aggregate=None):
         fut = self.execute(b"ZINTERSTORE", destkey, numkeys, *args)
         return fut
 
-    def zlexcount(self, key, min=b"-", max=b"+", include_min=True, include_max=True):
+    def zlexcount(self, key, min=b"-", max=b"+"):
         """Count the number of members in a sorted set between a given
         lexicographical range.
-
-        :raises TypeError: if min is not bytes
-        :raises TypeError: if max is not bytes
         """
-        if not isinstance(min, bytes):  # FIXME
-            raise TypeError("min argument must be bytes")
-        if not isinstance(max, bytes):  # FIXME     Why only bytes?
-            raise TypeError("max argument must be bytes")
-        if not min == b"-":
-            min = (b"[" if include_min else b"(") + min
-        if not max == b"+":
-            max = (b"[" if include_max else b"(") + max
         return self.execute(b"ZLEXCOUNT", key, min, max)
 
     def zrange(self, key, start=0, stop=-1, withscores=False, encoding=_NOTSET):
@@ -182,29 +171,16 @@ def zrangebylex(
         key,
         min=b"-",
         max=b"+",
-        include_min=True,
-        include_max=True,
         offset=None,
         count=None,
         encoding=_NOTSET,
     ):
         """Return a range of members in a sorted set, by lexicographical range.
 
-        :raises TypeError: if min is not bytes
-        :raises TypeError: if max is not bytes
         :raises TypeError: if both offset and count are not specified
-        :raises TypeError: if offset is not bytes
-        :raises TypeError: if count is not bytes
+        :raises TypeError: if offset is not int
+        :raises TypeError: if count is not int
         """
-        if not isinstance(min, bytes):  # FIXME
-            raise TypeError("min argument must be bytes")
-        if not isinstance(max, bytes):  # FIXME
-            raise TypeError("max argument must be bytes")
-        if not min == b"-":
-            min = (b"[" if include_min else b"(") + min
-        if not max == b"+":
-            max = (b"[" if include_max else b"(") + max
-
         if (offset is not None and count is None) or (
             count is not None and offset is None
         ):
@@ -273,23 +249,10 @@ def zrem(self, key, member, *members):
         """Remove one or more members from a sorted set."""
         return self.execute(b"ZREM", key, member, *members)
 
-    def zremrangebylex(
-        self, key, min=b"-", max=b"+", include_min=True, include_max=True
-    ):
+    def zremrangebylex(self, key, min=b"-", max=b"+"):
         """Remove all members in a sorted set between the given
         lexicographical range.
-
-        :raises TypeError: if min is not bytes
-        :raises TypeError: if max is not bytes
         """
-        if not isinstance(min, bytes):  # FIXME
-            raise TypeError("min argument must be bytes")
-        if not isinstance(max, bytes):  # FIXME
-            raise TypeError("max argument must be bytes")
-        if not min == b"-":
-            min = (b"[" if include_min else b"(") + min
-        if not max == b"+":
-            max = (b"[" if include_max else b"(") + max
         return self.execute(b"ZREMRANGEBYLEX", key, min, max)
 
     def zremrangebyrank(self, key, start, stop):
@@ -389,8 +352,6 @@ def zrevrangebylex(
         key,
         min=b"-",
         max=b"+",
-        include_min=True,
-        include_max=True,
         offset=None,
         count=None,
         encoding=_NOTSET,
@@ -398,21 +359,10 @@ def zrevrangebylex(
         """Return a range of members in a sorted set, by lexicographical range
         from high to low.
 
-        :raises TypeError: if min is not bytes
-        :raises TypeError: if max is not bytes
         :raises TypeError: if both offset and count are not specified
         :raises TypeError: if offset is not int
         :raises TypeError: if count is not int
         """
-        if not isinstance(min, bytes):  # FIXME
-            raise TypeError("min argument must be bytes")
-        if not isinstance(max, bytes):  # FIXME
-            raise TypeError("max argument must be bytes")
-        if not min == b"-":
-            min = (b"[" if include_min else b"(") + min
-        if not max == b"+":
-            max = (b"[" if include_max else b"(") + max
-
         if (offset is not None and count is None) or (
             count is not None and offset is None
         ):


Test patch:
diff --git a/tests/sorted_set_commands_test.py b/tests/sorted_set_commands_test.py
--- a/tests/sorted_set_commands_test.py
+++ b/tests/sorted_set_commands_test.py
@@ -245,20 +245,11 @@ async def test_zlexcount(redis):
     assert res == 5
     res = await redis.zlexcount(key)
     assert res == 5
-    res = await redis.zlexcount(key, min=b"-", max=b"e")
+    res = await redis.zlexcount(key, min=b"-", max=b"[e")
     assert res == 5
-    res = await redis.zlexcount(
-        key, min=b"a", max=b"e", include_min=False, include_max=False
-    )
+    res = await redis.zlexcount(key, min=b"(a", max=b"(e")
     assert res == 3
 
-    with pytest.raises(TypeError):
-        await redis.zlexcount(None, b"a", b"e")
-    with pytest.raises(TypeError):
-        await redis.zlexcount(key, 10, b"e")
-    with pytest.raises(TypeError):
-        await redis.zlexcount(key, b"a", 20)
-
 
 @pytest.mark.parametrize("encoding", [None, "utf-8"])
 @pytest.mark.asyncio
@@ -307,24 +298,22 @@ async def test_zrangebylex(redis):
     assert res == members
     res = await redis.zrangebylex(key, encoding="utf-8")
     assert res == strings
-    res = await redis.zrangebylex(key, min=b"-", max=b"d")
+    res = await redis.zrangebylex(key, min=b"-", max=b"[d")
     assert res == members[:-1]
-    res = await redis.zrangebylex(
-        key, min=b"a", max=b"e", include_min=False, include_max=False
-    )
+    res = await redis.zrangebylex(key, min=b"(a", max=b"(e")
     assert res == members[1:-1]
-    res = await redis.zrangebylex(key, min=b"x", max=b"z")
+    res = await redis.zrangebylex(key, min=b"[x", max=b"[z")
     assert res == []
-    res = await redis.zrangebylex(key, min=b"e", max=b"a")
+    res = await redis.zrangebylex(key, min=b"[e", max=b"[a")
     assert res == []
     res = await redis.zrangebylex(key, offset=1, count=2)
     assert res == members[1:3]
     with pytest.raises(TypeError):
-        await redis.zrangebylex(None, b"a", b"e")
+        await redis.zrangebylex(None, b"[a", b"[e")
     with pytest.raises(TypeError):
-        await redis.zrangebylex(key, 10, b"e")
+        await redis.zrangebylex(key, None, b"[e")
     with pytest.raises(TypeError):
-        await redis.zrangebylex(key, b"a", 20)
+        await redis.zrangebylex(key, b"[a", None)
     with pytest.raises(TypeError):
         await redis.zrangebylex(key, b"a", b"e", offset=1)
     with pytest.raises(TypeError):
@@ -446,16 +435,12 @@ async def test_zremrangebylex(redis):
     res = await redis.zadd(key, *pairs)
     assert res == 10
 
-    res = await redis.zremrangebylex(
-        key, b"alpha", b"omega", include_max=True, include_min=True
-    )
+    res = await redis.zremrangebylex(key, b"[alpha", b"[omega")
     assert res == 6
     res = await redis.zrange(key, 0, -1)
     assert res == [b"ALPHA", b"aaaa", b"zap", b"zip"]
 
-    res = await redis.zremrangebylex(
-        key, b"zap", b"zip", include_max=False, include_min=False
-    )
+    res = await redis.zremrangebylex(key, b"(zap", b"(zip")
     assert res == 0
 
     res = await redis.zrange(key, 0, -1)
@@ -469,9 +454,9 @@ async def test_zremrangebylex(redis):
     with pytest.raises(TypeError):
         await redis.zremrangebylex(None, b"a", b"e")
     with pytest.raises(TypeError):
-        await redis.zremrangebylex(key, 10, b"e")
+        await redis.zremrangebylex(key, None, b"e")
     with pytest.raises(TypeError):
-        await redis.zremrangebylex(key, b"a", 20)
+        await redis.zremrangebylex(key, b"a", None)
 
 
 @pytest.mark.asyncio
@@ -710,24 +695,22 @@ async def test_zrevrangebylex(redis):
     assert res == rev_members
     res = await redis.zrevrangebylex(key, encoding="utf-8")
     assert res == rev_strings
-    res = await redis.zrevrangebylex(key, min=b"-", max=b"d")
+    res = await redis.zrevrangebylex(key, min=b"-", max=b"[d")
     assert res == rev_members[1:]
-    res = await redis.zrevrangebylex(
-        key, min=b"a", max=b"e", include_min=False, include_max=False
-    )
+    res = await redis.zrevrangebylex(key, min=b"(a", max=b"(e")
     assert res == rev_members[1:-1]
-    res = await redis.zrevrangebylex(key, min=b"x", max=b"z")
+    res = await redis.zrevrangebylex(key, min=b"[x", max=b"[z")
     assert res == []
-    res = await redis.zrevrangebylex(key, min=b"e", max=b"a")
+    res = await redis.zrevrangebylex(key, min=b"[e", max=b"[a")
     assert res == []
     res = await redis.zrevrangebylex(key, offset=1, count=2)
     assert res == rev_members[1:3]
     with pytest.raises(TypeError):
         await redis.zrevrangebylex(None, b"a", b"e")
     with pytest.raises(TypeError):
-        await redis.zrevrangebylex(key, 10, b"e")
+        await redis.zrevrangebylex(key, None, b"e")
     with pytest.raises(TypeError):
-        await redis.zrevrangebylex(key, b"a", 20)
+        await redis.zrevrangebylex(key, b"a", None)
     with pytest.raises(TypeError):
         await redis.zrevrangebylex(key, b"a", b"e", offset=1)
     with pytest.raises(TypeError):