From a20e946abda035c2688fc7ccbe3d739175d6408e Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 11:04:11 +0200 Subject: [PATCH 1/7] add support to BYSCORE|BYLEX and to LIMIT to zrange. Also change ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANGEBYLEX and ZREVRANGEBYLEX to use ZRANGE --- redis/commands.py | 130 +++++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/redis/commands.py b/redis/commands.py index eb7cea54f6..0dfacf1ef5 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -1521,32 +1521,25 @@ def sort(self, name, start=None, num=None, by=None, get=None, pieces = [name] if by is not None: - pieces.append(b'BY') - pieces.append(by) + pieces.extend([b'BY', by]) if start is not None and num is not None: - pieces.append(b'LIMIT') - pieces.append(start) - pieces.append(num) + pieces.extend([b'LIMIT', start, num]) if get is not None: # If get is a string assume we want to get a single value. # Otherwise assume it's an interable and we want to get multiple # values. We can't just iterate blindly because strings are # iterable. if isinstance(get, (bytes, str)): - pieces.append(b'GET') - pieces.append(get) + pieces.extend([b'GET', get]) else: for g in get: - pieces.append(b'GET') - pieces.append(g) + pieces.extend([b'GET', g]) if desc: pieces.append(b'DESC') if alpha: pieces.append(b'ALPHA') if store is not None: - pieces.append(b'STORE') - pieces.append(store) - + pieces.extend([b'STORE', store]) if groups: if not get or isinstance(get, (bytes, str)) or len(get) < 2: raise DataError('when using "groups" the "get" argument ' @@ -1826,8 +1819,7 @@ def xadd(self, name, fields, id='*', maxlen=None, approximate=True, pieces.append(b'~') pieces.append(minid) if limit is not None: - pieces.append(b"LIMIT") - pieces.append(limit) + pieces.extend([b'LIMIT', limit]) if nomkstream: pieces.append(b'NOMKSTREAM') pieces.append(id) @@ -2417,24 +2409,40 @@ def bzpopmin(self, keys, timeout=0): return self.execute_command('BZPOPMIN', *keys) def zrange(self, name, start, end, desc=False, withscores=False, - score_cast_func=float): + score_cast_func=float, byscore=False, bylex=False, + offset=None, num=None): """ Return a range of values from sorted set ``name`` between ``start`` and ``end`` sorted in ascending order. ``start`` and ``end`` can be negative, indicating the end of the range. - ``desc`` a boolean indicating whether to sort the results descendingly + ``desc`` a boolean indicating whether to sort the results in descending order ``withscores`` indicates to return the scores along with the values. The return type is a list of (value, score) pairs ``score_cast_func`` a callable used to cast the score return value """ - if desc: - return self.zrevrange(name, start, end, withscores, - score_cast_func) + if byscore and bylex: + raise DataError("``byscore`` and ``bylex`` can be specified together.") + if byscore: + if desc: + return self.zrevrangebyscore(name, start, end, offset, num, + withscores, score_cast_func) + else: + return self.zrangebyscore(name, start, end, offset, num, + withscores, score_cast_func) + if bylex: + if desc: + return self.zrevrangebylex(name, start, end, offset, num, + withscores, score_cast_func) + else: + return self.zrangebylex(name, start, end, offset, num, + withscores, score_cast_func) pieces = ['ZRANGE', name, start, end] + if desc: + pieces.append(b'REV') if withscores: pieces.append(b'WITHSCORES') options = { @@ -2452,7 +2460,8 @@ def zrangestore(self, dest, name, start, end): """ return self.execute_command('ZRANGESTORE', dest, name, start, end) - def zrangebylex(self, name, min, max, start=None, num=None): + def zrangebylex(self, name, min, max, start=None, num=None, + withscores=False, score_cast_func=float): """ Return the lexicographical range of values from sorted set ``name`` between ``min`` and ``max``. @@ -2463,12 +2472,19 @@ def zrangebylex(self, name, min, max, start=None, num=None): 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] + pieces = ['ZRANGE', name, min, max, 'BYLEX'] if start is not None and num is not None: pieces.extend([b'LIMIT', start, num]) - return self.execute_command(*pieces) + if withscores: + pieces.append(b'WITHSCORES') + options = { + 'withscores': withscores, + 'score_cast_func': score_cast_func + } + return self.execute_command(*pieces, **options) - def zrevrangebylex(self, name, max, min, start=None, num=None): + def zrevrangebylex(self, name, max, min, start=None, num=None, + withscores=False, score_cast_func=float): """ Return the reversed lexicographical range of values from sorted set ``name`` between ``max`` and ``min``. @@ -2479,10 +2495,16 @@ def zrevrangebylex(self, name, max, min, start=None, num=None): 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] + pieces = ['ZRANGE', name, max, min, 'BYLEX', 'REV'] if start is not None and num is not None: pieces.extend([b'LIMIT', start, num]) - return self.execute_command(*pieces) + if withscores: + pieces.append(b'WITHSCORES') + options = { + 'withscores': withscores, + 'score_cast_func': score_cast_func + } + return self.execute_command(*pieces, **options) def zrangebyscore(self, name, min, max, start=None, num=None, withscores=False, score_cast_func=float): @@ -2501,7 +2523,35 @@ def zrangebyscore(self, name, min, max, start=None, num=None, 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] + pieces = ['ZRANGE', name, min, max, 'BYSCORE'] + if start is not None and num is not None: + pieces.extend([b'LIMIT', start, num]) + if withscores: + pieces.append(b'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): + """ + Return a range of values from the sorted set ``name`` with scores + between ``min`` and ``max`` in descending order. + + If ``start`` and ``num`` are specified, then return a slice + of the range. + + ``withscores`` indicates to return the scores along with the values. + The return type is a list of (value, score) pairs + + ``score_cast_func`` a callable used to cast the score return value + """ + 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 = ['ZRANGE', name, max, min, 'BYSCORE', 'REV'] if start is not None and num is not None: pieces.extend([b'LIMIT', start, num]) if withscores: @@ -2570,34 +2620,6 @@ def zrevrange(self, name, start, end, withscores=False, } return self.execute_command(*pieces, **options) - def zrevrangebyscore(self, name, max, min, start=None, num=None, - withscores=False, score_cast_func=float): - """ - Return a range of values from the sorted set ``name`` with scores - between ``min`` and ``max`` in descending order. - - If ``start`` and ``num`` are specified, then return a slice - of the range. - - ``withscores`` indicates to return the scores along with the values. - The return type is a list of (value, score) pairs - - ``score_cast_func`` a callable used to cast the score return value - """ - 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([b'LIMIT', start, num]) - if withscores: - pieces.append(b'WITHSCORES') - options = { - 'withscores': withscores, - 'score_cast_func': score_cast_func - } - return self.execute_command(*pieces, **options) - def zrevrank(self, name, value): """ Returns a 0-based value indicating the descending rank of From b3c6bdff2c24c3fda7af7fa8cede737bd21d07cc Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 11:57:26 +0200 Subject: [PATCH 2/7] change ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANGEBYLEX and ZREVRANGEBYLEX to use ZRANGE --- redis/commands.py | 142 ++++++++++++++++------------------------------ 1 file changed, 48 insertions(+), 94 deletions(-) diff --git a/redis/commands.py b/redis/commands.py index 0dfacf1ef5..46269febcb 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -2417,40 +2417,64 @@ def zrange(self, name, start, end, desc=False, withscores=False, ``start`` and ``end`` can be negative, indicating the end of the range. - ``desc`` a boolean indicating whether to sort the results in descending order + ``desc`` a boolean indicating whether to sort the results in reversed order ``withscores`` indicates to return the scores along with the values. The return type is a list of (value, score) pairs ``score_cast_func`` a callable used to cast the score return value + + ``byscore`` when set to True, returns the range of elements from the sorted + set having scores equal or between ``start`` and ``end``. + + ``bylex`` when set to True, returns the range of elements from the sorted + set between the ``start`` and ``end`` lexicographical closed range intervals. + Valid ``start`` and ``end`` must start with ( or [, in order to specify whether + the range interval is exclusive or inclusive, respectively. + + ``offset`` and ``num`` are specified, then return a slice of the range. + Can't be provided when using ``bylex``. """ if byscore and bylex: raise DataError("``byscore`` and ``bylex`` can be specified together.") + if (offset is not None and num is None) or \ + (num is not None and offset is None): + raise DataError("``offset`` and ``num`` must both be specified") + if bylex and withscores: + raise DataError("``withscores`` not supported in combination with ``bylex``") + pieces = ['ZRANGE', name, start, end] if byscore: - if desc: - return self.zrevrangebyscore(name, start, end, offset, num, - withscores, score_cast_func) - else: - return self.zrangebyscore(name, start, end, offset, num, - withscores, score_cast_func) + pieces.append('BYSCORE') if bylex: - if desc: - return self.zrevrangebylex(name, start, end, offset, num, - withscores, score_cast_func) - else: - return self.zrangebylex(name, start, end, offset, num, - withscores, score_cast_func) - pieces = ['ZRANGE', name, start, end] + pieces.append('BYLEX') if desc: - pieces.append(b'REV') + pieces.append('REV') + if offset and num: + pieces.extend(['LIMIT', offset, num]) if withscores: - pieces.append(b'WITHSCORES') + pieces.append('WITHSCORES') options = { 'withscores': withscores, 'score_cast_func': score_cast_func } return self.execute_command(*pieces, **options) + def zrevrange(self, name, start, end, withscores=False, + score_cast_func=float): + """ + Return a range of values from sorted set ``name`` between + ``start`` and ``end`` sorted in descending order. + + ``start`` and ``end`` can be negative, indicating the end of the range. + + ``withscores`` indicates to return the scores along with the values + The return type is a list of (value, score) pairs + + ``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) + def zrangestore(self, dest, name, start, end): """ Stores in ``dest`` the result of a range of values from sorted set @@ -2460,8 +2484,7 @@ def zrangestore(self, dest, name, start, end): """ return self.execute_command('ZRANGESTORE', dest, name, start, end) - def zrangebylex(self, name, min, max, start=None, num=None, - withscores=False, score_cast_func=float): + def zrangebylex(self, name, min, max, start=None, num=None): """ Return the lexicographical range of values from sorted set ``name`` between ``min`` and ``max``. @@ -2469,22 +2492,9 @@ def zrangebylex(self, name, min, max, start=None, num=None, If ``start`` and ``num`` are specified, then return a slice of the range. """ - 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 = ['ZRANGE', name, min, max, 'BYLEX'] - if start is not None and num is not None: - pieces.extend([b'LIMIT', start, num]) - if withscores: - pieces.append(b'WITHSCORES') - options = { - 'withscores': withscores, - 'score_cast_func': score_cast_func - } - return self.execute_command(*pieces, **options) + return self.zrange(name, min, max, bylex=True, offset=start, num=num) - def zrevrangebylex(self, name, max, min, start=None, num=None, - withscores=False, score_cast_func=float): + def zrevrangebylex(self, name, max, min, start=None, num=None): """ Return the reversed lexicographical range of values from sorted set ``name`` between ``max`` and ``min``. @@ -2492,19 +2502,7 @@ def zrevrangebylex(self, name, max, min, start=None, num=None, If ``start`` and ``num`` are specified, then return a slice of the range. """ - 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 = ['ZRANGE', name, max, min, 'BYLEX', 'REV'] - if start is not None and num is not None: - pieces.extend([b'LIMIT', start, num]) - if withscores: - pieces.append(b'WITHSCORES') - options = { - 'withscores': withscores, - 'score_cast_func': score_cast_func - } - return self.execute_command(*pieces, **options) + return self.zrange(name, max, min, desc=True, bylex=True, offset=start, num=num) def zrangebyscore(self, name, min, max, start=None, num=None, withscores=False, score_cast_func=float): @@ -2520,19 +2518,8 @@ def zrangebyscore(self, name, min, max, start=None, num=None, `score_cast_func`` a callable used to cast the score return value """ - 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 = ['ZRANGE', name, min, max, 'BYSCORE'] - if start is not None and num is not None: - pieces.extend([b'LIMIT', start, num]) - if withscores: - pieces.append(b'WITHSCORES') - options = { - 'withscores': withscores, - 'score_cast_func': score_cast_func - } - return self.execute_command(*pieces, **options) + return self.zrange(name, min, max, byscore=True, offset=start, num=num, + withscores=withscores, score_cast_func=score_cast_func) def zrevrangebyscore(self, name, max, min, start=None, num=None, withscores=False, score_cast_func=float): @@ -2548,19 +2535,8 @@ def zrevrangebyscore(self, name, max, min, start=None, num=None, ``score_cast_func`` a callable used to cast the score return value """ - 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 = ['ZRANGE', name, max, min, 'BYSCORE', 'REV'] - if start is not None and num is not None: - pieces.extend([b'LIMIT', start, num]) - if withscores: - pieces.append(b'WITHSCORES') - options = { - 'withscores': withscores, - 'score_cast_func': score_cast_func - } - return self.execute_command(*pieces, **options) + return self.zrange(name, max, min, desc=True, byscore=True, offset=start, num=num, + withscores=withscores, score_cast_func=score_cast_func) def zrank(self, name, value): """ @@ -2598,28 +2574,6 @@ def zremrangebyscore(self, name, min, max): """ return self.execute_command('ZREMRANGEBYSCORE', name, min, max) - def zrevrange(self, name, start, end, withscores=False, - score_cast_func=float): - """ - Return a range of values from sorted set ``name`` between - ``start`` and ``end`` sorted in descending order. - - ``start`` and ``end`` can be negative, indicating the end of the range. - - ``withscores`` indicates to return the scores along with the values - The return type is a list of (value, score) pairs - - ``score_cast_func`` a callable used to cast the score return value - """ - 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 zrevrank(self, name, value): """ Returns a 0-based value indicating the descending rank of From 4616d89fd760936bf0fc198597ee95797066a363 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 11:59:52 +0200 Subject: [PATCH 3/7] typo --- redis/commands.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redis/commands.py b/redis/commands.py index 46269febcb..5a4ec74a30 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -2417,12 +2417,12 @@ def zrange(self, name, start, end, desc=False, withscores=False, ``start`` and ``end`` can be negative, indicating the end of the range. - ``desc`` a boolean indicating whether to sort the results in reversed order + ``desc`` a boolean indicating whether to sort the results in reversed order. ``withscores`` indicates to return the scores along with the values. - The return type is a list of (value, score) pairs + The return type is a list of (value, score) pairs. - ``score_cast_func`` a callable used to cast the score return value + ``score_cast_func`` a callable used to cast the score return value. ``byscore`` when set to True, returns the range of elements from the sorted set having scores equal or between ``start`` and ``end``. @@ -2436,12 +2436,12 @@ def zrange(self, name, start, end, desc=False, withscores=False, Can't be provided when using ``bylex``. """ if byscore and bylex: - raise DataError("``byscore`` and ``bylex`` can be specified together.") + raise DataError("``byscore`` and ``bylex`` can not be specified together.") if (offset is not None and num is None) or \ (num is not None and offset is None): - raise DataError("``offset`` and ``num`` must both be specified") + raise DataError("``offset`` and ``num`` must both be specified.") if bylex and withscores: - raise DataError("``withscores`` not supported in combination with ``bylex``") + raise DataError("``withscores`` not supported in combination with ``bylex``.") pieces = ['ZRANGE', name, start, end] if byscore: pieces.append('BYSCORE') From 178fd77d093448c8d3e5d37a5eeb3fffb56ab8c9 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 12:09:31 +0200 Subject: [PATCH 4/7] test --- tests/test_commands.py | 46 +++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 2be8923e0e..4514271621 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1840,6 +1840,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'] # withscores assert r.zrange('a', 0, 1, withscores=True) == \ @@ -1851,6 +1852,44 @@ def test_zrange(self, r): assert r.zrange('a', 0, 1, withscores=True, score_cast_func=int) == \ [(b'a1', 1), (b'a2', 2)] + def test_zrange_errors(self, r): + with pytest.raises(exceptions.DataError): + r.zrange('a', 0, 1, byscore=True, bylex=True) + with pytest.raises(exceptions.DataError): + r.zrange('a', 0, 1, bylex=True, withscores=True) + with pytest.raises(exceptions.DataError): + r.zrange('a', 0, 1, byscore=True, withscores=True, offset=4) + with pytest.raises(exceptions.DataError): + r.zrange('a', 0, 1, byscore=True, withscores=True, num=2) + + @skip_if_server_version_lt('6.2.0') + def test_zrange_params(self, r): + # bylex + r.zadd('a', {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'f': 0, 'g': 0}) + assert r.zrange('a', '[aaa', '(g', bylex=True) == \ + [b'b', b'c', b'd', b'e', b'f'] + assert r.zrange('a', '[f', '+', bylex=True) == [b'f', b'g'] + assert r.zrange('a', '+', '[f', desc=True, bylex=True) == [b'g', b'f'] + assert r.zrange('a', '-', '+', bylex=True, offset=3, num=2) == \ + [b'd', b'e'] + assert r.zrange('a', '+', '-', desc=True, bylex=True, offset=3, num=2) == \ + [b'd', b'c'] + + # byscore + r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5}) + assert r.zrange('a', 2, 4, byscore=True, offset=1, num=2) == \ + [b'a3', b'a4'] + assert r.zrange('a', 4, 2, desc=True, byscore=True, offset=1, num=2) == \ + [b'a3', b'a2'] + assert r.zrange('a', 2, 4, byscore=True, withscores=True) == \ + [(b'a2', 2.0), (b'a3', 3.0), (b'a4', 4.0)] + assert r.zrange('a', 4, 2, desc=True, byscore=True, withscores=True, + score_cast_func=int) == \ + [(b'a4', 4), (b'a3', 3), (b'a2', 2)] + + # rev + assert r.zrange('a', 0, 1, desc=True) == [b'a5', b'a4'] + @skip_if_server_version_lt('6.2.0') def test_zrangestore(self, r): r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3}) @@ -1885,16 +1924,12 @@ def test_zrevrangebylex(self, r): def test_zrangebyscore(self, r): r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5}) assert r.zrangebyscore('a', 2, 4) == [b'a2', b'a3', b'a4'] - # slicing with start/num assert r.zrangebyscore('a', 2, 4, start=1, num=2) == \ [b'a3', b'a4'] - # withscores assert r.zrangebyscore('a', 2, 4, withscores=True) == \ [(b'a2', 2.0), (b'a3', 3.0), (b'a4', 4.0)] - - # custom score function assert r.zrangebyscore('a', 2, 4, withscores=True, score_cast_func=int) == \ [(b'a2', 2), (b'a3', 3), (b'a4', 4)] @@ -1958,15 +1993,12 @@ def test_zrevrange(self, r): def test_zrevrangebyscore(self, r): r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5}) assert r.zrevrangebyscore('a', 4, 2) == [b'a4', b'a3', b'a2'] - # slicing with start/num assert r.zrevrangebyscore('a', 4, 2, start=1, num=2) == \ [b'a3', b'a2'] - # withscores assert r.zrevrangebyscore('a', 4, 2, withscores=True) == \ [(b'a4', 4.0), (b'a3', 3.0), (b'a2', 2.0)] - # custom score function assert r.zrevrangebyscore('a', 4, 2, withscores=True, score_cast_func=int) == \ From 3998bf5a63be3b71c097950fd0637fbcb9737498 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 12:22:44 +0200 Subject: [PATCH 5/7] flake8 --- redis/commands.py | 40 +++++++++++++++++++++++++--------------- tests/test_commands.py | 10 ++++++---- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/redis/commands.py b/redis/commands.py index 5a4ec74a30..bbb3e280d3 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -2417,31 +2417,35 @@ def zrange(self, name, start, end, desc=False, withscores=False, ``start`` and ``end`` can be negative, indicating the end of the range. - ``desc`` a boolean indicating whether to sort the results in reversed order. + ``desc`` a boolean indicating whether to sort the results in reversed + order. ``withscores`` indicates to return the scores along with the values. The return type is a list of (value, score) pairs. ``score_cast_func`` a callable used to cast the score return value. - ``byscore`` when set to True, returns the range of elements from the sorted - set having scores equal or between ``start`` and ``end``. + ``byscore`` when set to True, returns the range of elements from the + sorted set having scores equal or between ``start`` and ``end``. - ``bylex`` when set to True, returns the range of elements from the sorted - set between the ``start`` and ``end`` lexicographical closed range intervals. - Valid ``start`` and ``end`` must start with ( or [, in order to specify whether - the range interval is exclusive or inclusive, respectively. + ``bylex`` when set to True, returns the range of elements from the + sorted set between the ``start`` and ``end`` lexicographical closed + range intervals. + Valid ``start`` and ``end`` must start with ( or [, in order to specify + whether the range interval is exclusive or inclusive, respectively. ``offset`` and ``num`` are specified, then return a slice of the range. Can't be provided when using ``bylex``. """ if byscore and bylex: - raise DataError("``byscore`` and ``bylex`` can not be specified together.") + raise DataError("``byscore`` and ``bylex`` can not be " + "specified together.") if (offset is not None and num is None) or \ (num is not None and offset is None): raise DataError("``offset`` and ``num`` must both be specified.") if bylex and withscores: - raise DataError("``withscores`` not supported in combination with ``bylex``.") + raise DataError("``withscores`` not supported in combination " + "with ``bylex``.") pieces = ['ZRANGE', name, start, end] if byscore: pieces.append('BYSCORE') @@ -2473,7 +2477,8 @@ 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) + withscores=withscores, + score_cast_func=score_cast_func) def zrangestore(self, dest, name, start, end): """ @@ -2502,7 +2507,8 @@ 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) + return self.zrange(name, max, min, desc=True, + bylex=True, offset=start, num=num) def zrangebyscore(self, name, min, max, start=None, num=None, withscores=False, score_cast_func=float): @@ -2518,8 +2524,10 @@ 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) + return self.zrange(name, min, max, byscore=True, + offset=start, num=num, + withscores=withscores, + score_cast_func=score_cast_func) def zrevrangebyscore(self, name, max, min, start=None, num=None, withscores=False, score_cast_func=float): @@ -2535,8 +2543,10 @@ 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) + return self.zrange(name, max, min, desc=True, + byscore=True, offset=start, + num=num, withscores=withscores, + score_cast_func=score_cast_func) def zrank(self, name, value): """ diff --git a/tests/test_commands.py b/tests/test_commands.py index 4514271621..265a2d5522 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1872,19 +1872,21 @@ def test_zrange_params(self, r): assert r.zrange('a', '+', '[f', desc=True, bylex=True) == [b'g', b'f'] assert r.zrange('a', '-', '+', bylex=True, offset=3, num=2) == \ [b'd', b'e'] - assert r.zrange('a', '+', '-', desc=True, bylex=True, offset=3, num=2) == \ + assert r.zrange('a', '+', '-', desc=True, bylex=True, + offset=3, num=2) == \ [b'd', b'c'] # byscore r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5}) assert r.zrange('a', 2, 4, byscore=True, offset=1, num=2) == \ [b'a3', b'a4'] - assert r.zrange('a', 4, 2, desc=True, byscore=True, offset=1, num=2) == \ + assert r.zrange('a', 4, 2, desc=True, byscore=True, + offset=1, num=2) == \ [b'a3', b'a2'] assert r.zrange('a', 2, 4, byscore=True, withscores=True) == \ [(b'a2', 2.0), (b'a3', 3.0), (b'a4', 4.0)] - assert r.zrange('a', 4, 2, desc=True, byscore=True, withscores=True, - score_cast_func=int) == \ + assert r.zrange('a', 4, 2, desc=True, byscore=True, + withscores=True, score_cast_func=int) == \ [(b'a4', 4), (b'a3', 3), (b'a2', 2)] # rev From c3c101b8990ce580c9b3424053c9d75ccc5a30c4 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 13:30:32 +0200 Subject: [PATCH 6/7] zrangestore --- redis/commands.py | 99 +++++++++++++++++++++++++++--------------- tests/test_commands.py | 10 +++++ 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/redis/commands.py b/redis/commands.py index bbb3e280d3..f8e2647d03 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -1049,7 +1049,7 @@ def hrandfield(self, key, count=None, withvalues=False): return self.execute_command("HRANDFIELD", key, *params) def randomkey(self): - "Returns the name of a random key" + """Returns the name of a random key""" return self.execute_command('RANDOMKEY') def rename(self, src, dst): @@ -1059,7 +1059,7 @@ def rename(self, src, dst): return self.execute_command('RENAME', src, dst) def renamenx(self, src, dst): - "Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist" + """Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist""" return self.execute_command('RENAMENX', src, dst) def restore(self, name, ttl, value, replace=False, absttl=False, @@ -1698,15 +1698,15 @@ def zscan_iter(self, name, match=None, count=None, # SET COMMANDS def sadd(self, name, *values): - "Add ``value(s)`` to set ``name``" + """Add ``value(s)`` to set ``name``""" return self.execute_command('SADD', name, *values) def scard(self, name): - "Return the number of elements in set ``name``" + """Return the number of elements in set ``name``""" return self.execute_command('SCARD', name) def sdiff(self, keys, *args): - "Return the difference of sets specified by ``keys``" + """Return the difference of sets specified by ``keys``""" args = list_or_args(keys, args) return self.execute_command('SDIFF', *args) @@ -1719,7 +1719,7 @@ def sdiffstore(self, dest, keys, *args): return self.execute_command('SDIFFSTORE', dest, *args) def sinter(self, keys, *args): - "Return the intersection of sets specified by ``keys``" + """Return the intersection of sets specified by ``keys``""" args = list_or_args(keys, args) return self.execute_command('SINTER', *args) @@ -1732,15 +1732,15 @@ def sinterstore(self, dest, keys, *args): return self.execute_command('SINTERSTORE', dest, *args) def sismember(self, name, value): - "Return a boolean indicating if ``value`` is a member of set ``name``" + """Return a boolean indicating if ``value`` is a member of set ``name``""" return self.execute_command('SISMEMBER', name, value) def smembers(self, name): - "Return all members of the set ``name``" + """Return all members of the set ``name``""" return self.execute_command('SMEMBERS', name) def smove(self, src, dst, value): - "Move ``value`` from set ``src`` to set ``dst`` atomically" + """Move ``value`` from set ``src`` to set ``dst`` atomically""" return self.execute_command('SMOVE', src, dst, value) def spop(self, name, count=None): @@ -2408,6 +2408,38 @@ def bzpopmin(self, keys, timeout=0): keys.append(timeout) return self.execute_command('BZPOPMIN', *keys) + def _zrange(self, command, dest, name, start, end, desc=False, + byscore=False, bylex=False, withscores=False, + score_cast_func=float, offset=None, num=None): + if byscore and bylex: + raise DataError("``byscore`` and ``bylex`` can not be " + "specified together.") + if (offset is not None and num is None) or \ + (num is not None and offset is None): + raise DataError("``offset`` and ``num`` must both be specified.") + if bylex and withscores: + raise DataError("``withscores`` not supported in combination " + "with ``bylex``.") + pieces = [command] + if dest: + pieces.append(dest) + pieces.extend([name, start, end]) + if byscore: + pieces.append('BYSCORE') + if bylex: + pieces.append('BYLEX') + if desc: + pieces.append('REV') + if offset is not None and num is not None: + pieces.extend(['LIMIT', offset, num]) + if withscores: + pieces.append('WITHSCORES') + options = { + 'withscores': withscores, + 'score_cast_func': score_cast_func + } + return self.execute_command(*pieces, **options) + def zrange(self, name, start, end, desc=False, withscores=False, score_cast_func=float, byscore=False, bylex=False, offset=None, num=None): @@ -2437,31 +2469,8 @@ def zrange(self, name, start, end, desc=False, withscores=False, ``offset`` and ``num`` are specified, then return a slice of the range. Can't be provided when using ``bylex``. """ - if byscore and bylex: - raise DataError("``byscore`` and ``bylex`` can not be " - "specified together.") - if (offset is not None and num is None) or \ - (num is not None and offset is None): - raise DataError("``offset`` and ``num`` must both be specified.") - if bylex and withscores: - raise DataError("``withscores`` not supported in combination " - "with ``bylex``.") - pieces = ['ZRANGE', name, start, end] - if byscore: - pieces.append('BYSCORE') - if bylex: - pieces.append('BYLEX') - if desc: - pieces.append('REV') - if offset and num: - pieces.extend(['LIMIT', offset, num]) - if withscores: - pieces.append('WITHSCORES') - options = { - 'withscores': withscores, - 'score_cast_func': score_cast_func - } - return self.execute_command(*pieces, **options) + return self._zrange('ZRANGE', None, name, start, end, desc, byscore, + bylex, withscores, score_cast_func, offset, num) def zrevrange(self, name, start, end, withscores=False, score_cast_func=float): @@ -2480,14 +2489,32 @@ def zrevrange(self, name, start, end, withscores=False, withscores=withscores, score_cast_func=score_cast_func) - def zrangestore(self, dest, name, start, end): + def zrangestore(self, dest, name, start, end, + byscore=False, bylex=False, desc=False, + offset=None, num=None): """ Stores in ``dest`` the result of a range of values from sorted set ``name`` between ``start`` and ``end`` sorted in ascending order. ``start`` and ``end`` can be negative, indicating the end of the range. + + ``byscore`` when set to True, returns the range of elements from the + sorted set having scores equal or between ``start`` and ``end``. + + ``bylex`` when set to True, returns the range of elements from the + sorted set between the ``start`` and ``end`` lexicographical closed + range intervals. + Valid ``start`` and ``end`` must start with ( or [, in order to specify + whether the range interval is exclusive or inclusive, respectively. + + ``desc`` a boolean indicating whether to sort the results in reversed + order. + + ``offset`` and ``num`` are specified, then return a slice of the range. + Can't be provided when using ``bylex``. """ - return self.execute_command('ZRANGESTORE', dest, name, start, end) + return self._zrange('ZRANGESTORE', dest, name, start, end, desc, byscore, + bylex, False, None, offset, num) def zrangebylex(self, name, min, max, start=None, num=None): """ diff --git a/tests/test_commands.py b/tests/test_commands.py index 265a2d5522..d6e569d5ee 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1901,6 +1901,16 @@ def test_zrangestore(self, r): assert r.zrange('b', 0, -1) == [b'a2', b'a3'] assert r.zrange('b', 0, -1, withscores=True) == \ [(b'a2', 2), (b'a3', 3)] + # reversed order + assert r.zrangestore('b', 'a', 1, 2, desc=True) + assert r.zrange('b', 0, -1) == [b'a1', b'a2'] + # by score + assert r.zrangestore('b', 'a', 1, 2, byscore=True, + offset=0, num=1) + assert r.zrange('b', 0, -1) == [b'a1'] + # by lex + assert r.zrange('a', '[a2', '(a3', bylex=True) == \ + [b'a2'] @skip_if_server_version_lt('2.8.9') def test_zrangebylex(self, r): From 8556a7636820101a619afeabb67c54d0f4c91e52 Mon Sep 17 00:00:00 2001 From: AvitalFineRedis Date: Wed, 13 Oct 2021 13:31:35 +0200 Subject: [PATCH 7/7] flake8 --- redis/commands.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/redis/commands.py b/redis/commands.py index f8e2647d03..a00fa9a647 100644 --- a/redis/commands.py +++ b/redis/commands.py @@ -1732,7 +1732,9 @@ def sinterstore(self, dest, keys, *args): return self.execute_command('SINTERSTORE', dest, *args) def sismember(self, name, value): - """Return a boolean indicating if ``value`` is a member of set ``name``""" + """ + Return a boolean indicating if ``value`` is a member of set ``name`` + """ return self.execute_command('SISMEMBER', name, value) def smembers(self, name): @@ -2470,7 +2472,7 @@ def zrange(self, name, start, end, desc=False, withscores=False, Can't be provided when using ``bylex``. """ return self._zrange('ZRANGE', None, name, start, end, desc, byscore, - bylex, withscores, score_cast_func, offset, num) + bylex, withscores, score_cast_func, offset, num) def zrevrange(self, name, start, end, withscores=False, score_cast_func=float): @@ -2513,8 +2515,8 @@ def zrangestore(self, dest, name, start, end, ``offset`` and ``num`` are specified, then return a slice of the range. Can't be provided when using ``bylex``. """ - return self._zrange('ZRANGESTORE', dest, name, start, end, desc, byscore, - bylex, False, None, offset, num) + return self._zrange('ZRANGESTORE', dest, name, start, end, desc, + byscore, bylex, False, None, offset, num) def zrangebylex(self, name, min, max, start=None, num=None): """