Repository: redis/redis-py

Issue:
4.0.0rc1 breaks various zrange commands on older servers
**Version**: redis-py 4.0.0rc1, redis 6.0.16 (or any less than 6.2).

**Platform**: Python 3.8, Ubuntu 20.04

**Description**:

Previously calling `zrevrange` or `zrange(..., desc=True)` would use ZREVRANGE on the wire. Somewhere between 4.0.0b1 and 4.0.0rc1 this changed to use `ZRANGE ... REV`, which is new syntax in Redis 6.2. When using an older server this causes a syntax error.

To reproduce:
```python
import redis
r = redis.Redis('localhost')
r.zrevrange('foo', 0, -1)
```

Other commands are also affected e.g. zrangebyscore and zrangebylex are also using ZRANGE with options on the wire which is not supported pre-6.2.


Discussion:
@chayim looks like this was broken by #1603.
Thanks @bmerry for tracking this down all the way. We'll definitely get this into rc2!  @AvitalFineRedis while we're here, let's test for this case.



Resolving patch:
diff --git a/redis/commands/core.py b/redis/commands/core.py
--- a/redis/commands/core.py
+++ b/redis/commands/core.py
@@ -2536,9 +2536,14 @@ def zrevrange(self, name, start, end, withscores=False,
 
         ``score_cast_func`` a callable used to cast the score return value
         """
-        return self.zrange(name, start, end, desc=True,
-                           withscores=withscores,
-                           score_cast_func=score_cast_func)
+        pieces = ['ZREVRANGE', name, start, end]
+        if withscores:
+            pieces.append(b'WITHSCORES')
+        options = {
+            'withscores': withscores,
+            'score_cast_func': score_cast_func
+        }
+        return self.execute_command(*pieces, **options)
 
     def zrangestore(self, dest, name, start, end,
                     byscore=False, bylex=False, desc=False,
@@ -2575,7 +2580,13 @@ def zrangebylex(self, name, min, max, start=None, num=None):
         If ``start`` and ``num`` are specified, then return a slice of the
         range.
         """
-        return self.zrange(name, min, max, bylex=True, offset=start, num=num)
+        if (start is not None and num is None) or \
+                (num is not None and start is None):
+            raise DataError("``start`` and ``num`` must both be specified")
+        pieces = ['ZRANGEBYLEX', name, min, max]
+        if start is not None and num is not None:
+            pieces.extend([b'LIMIT', start, num])
+        return self.execute_command(*pieces)
 
     def zrevrangebylex(self, name, max, min, start=None, num=None):
         """
@@ -2585,8 +2596,13 @@ def zrevrangebylex(self, name, max, min, start=None, num=None):
         If ``start`` and ``num`` are specified, then return a slice of the
         range.
         """
-        return self.zrange(name, max, min, desc=True,
-                           bylex=True, offset=start, num=num)
+        if (start is not None and num is None) or \
+                (num is not None and start is None):
+            raise DataError("``start`` and ``num`` must both be specified")
+        pieces = ['ZREVRANGEBYLEX', name, max, min]
+        if start is not None and num is not None:
+            pieces.extend(['LIMIT', start, num])
+        return self.execute_command(*pieces)
 
     def zrangebyscore(self, name, min, max, start=None, num=None,
                       withscores=False, score_cast_func=float):
@@ -2602,10 +2618,19 @@ def zrangebyscore(self, name, min, max, start=None, num=None,
 
         `score_cast_func`` a callable used to cast the score return value
         """
-        return self.zrange(name, min, max, byscore=True,
-                           offset=start, num=num,
-                           withscores=withscores,
-                           score_cast_func=score_cast_func)
+        if (start is not None and num is None) or \
+                (num is not None and start is None):
+            raise DataError("``start`` and ``num`` must both be specified")
+        pieces = ['ZRANGEBYSCORE', name, min, max]
+        if start is not None and num is not None:
+            pieces.extend(['LIMIT', start, num])
+        if withscores:
+            pieces.append('WITHSCORES')
+        options = {
+            'withscores': withscores,
+            'score_cast_func': score_cast_func
+        }
+        return self.execute_command(*pieces, **options)
 
     def zrevrangebyscore(self, name, max, min, start=None, num=None,
                          withscores=False, score_cast_func=float):
@@ -2621,10 +2646,19 @@ def zrevrangebyscore(self, name, max, min, start=None, num=None,
 
         ``score_cast_func`` a callable used to cast the score return value
         """
-        return self.zrange(name, max, min, desc=True,
-                           byscore=True, offset=start,
-                           num=num, withscores=withscores,
-                           score_cast_func=score_cast_func)
+        if (start is not None and num is None) or \
+                (num is not None and start is None):
+            raise DataError("``start`` and ``num`` must both be specified")
+        pieces = ['ZREVRANGEBYSCORE', name, max, min]
+        if start is not None and num is not None:
+            pieces.extend(['LIMIT', start, num])
+        if withscores:
+            pieces.append('WITHSCORES')
+        options = {
+            'withscores': withscores,
+            'score_cast_func': score_cast_func
+        }
+        return self.execute_command(*pieces, **options)
 
     def zrank(self, name, value):
         """


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -1866,7 +1866,7 @@ def test_zrange(self, r):
         r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3})
         assert r.zrange('a', 0, 1) == [b'a1', b'a2']
         assert r.zrange('a', 1, 2) == [b'a2', b'a3']
-        assert r.zrange('a', 0, 2, desc=True) == [b'a3', b'a2', b'a1']
+        assert r.zrange('a', 0, 2) == [b'a1', b'a2', b'a3']
 
         # withscores
         assert r.zrange('a', 0, 1, withscores=True) == \