Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions redis/_parsers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,39 @@ def zset_score_pairs(response, **options):
return list(zip(it, map(score_cast_func, it)))


def zset_score_for_rank(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a list of (value, score) pairs
"""
if not response or not options.get("withscore"):
return response
score_cast_func = options.get("score_cast_func", float)
return [response[0], score_cast_func(response[1])]


def zset_score_pairs_resp3(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a list of (value, score) pairs
"""
if not response or not options.get("withscores"):
return response
score_cast_func = options.get("score_cast_func", float)
return [[name, score_cast_func(val)] for name, val in response]


def zset_score_for_rank_resp3(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a list of (value, score) pairs
"""
if not response or not options.get("withscore"):
return response
score_cast_func = options.get("score_cast_func", float)
return [response[0], score_cast_func(response[1])]


def sort_return_tuples(response, **options):
"""
If ``groups`` is specified, return the response as a list of
Expand Down Expand Up @@ -797,10 +830,14 @@ def string_keys_to_dict(key_string, callback):
"SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
),
**string_keys_to_dict(
"ZDIFF ZINTER ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYSCORE ZRANK ZREVRANGE "
"ZREVRANGEBYSCORE ZREVRANK ZUNION",
"ZDIFF ZINTER ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYSCORE ZREVRANGE "
"ZREVRANGEBYSCORE ZUNION",
zset_score_pairs,
),
**string_keys_to_dict(
"ZREVRANK ZRANK",
zset_score_for_rank,
),
**string_keys_to_dict("ZINCRBY ZSCORE", float_or_none),
**string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True),
**string_keys_to_dict("BLPOP BRPOP", lambda r: r and tuple(r) or None),
Expand Down Expand Up @@ -844,10 +881,17 @@ def string_keys_to_dict(key_string, callback):
"SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
),
**string_keys_to_dict(
"ZRANGE ZINTER ZPOPMAX ZPOPMIN ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE "
"ZUNION HGETALL XREADGROUP",
"ZRANGE ZINTER ZPOPMAX ZPOPMIN HGETALL XREADGROUP",
lambda r, **kwargs: r,
),
**string_keys_to_dict(
"ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
zset_score_pairs_resp3,
),
**string_keys_to_dict(
"ZREVRANK ZRANK",
zset_score_for_rank_resp3,
),
**string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3),
"ACL LOG": lambda r: (
[
Expand Down
36 changes: 29 additions & 7 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4778,18 +4778,25 @@ def zrank(
name: KeyT,
value: EncodableT,
withscore: bool = False,
score_cast_func: Union[type, Callable] = float,
) -> ResponseT:
"""
Returns a 0-based value indicating the rank of ``value`` in sorted set
``name``.
The optional WITHSCORE argument supplements the command's
reply with the score of the element returned.

``score_cast_func`` a callable used to cast the score return value

For more information, see https://redis.io/commands/zrank
"""
pieces = ["ZRANK", name, value]
if withscore:
return self.execute_command("ZRANK", name, value, "WITHSCORE", keys=[name])
return self.execute_command("ZRANK", name, value, keys=[name])
pieces.append("WITHSCORE")

options = {"withscore": withscore, "score_cast_func": score_cast_func}

return self.execute_command(*pieces, **options)

def zrem(self, name: KeyT, *values: FieldT) -> ResponseT:
"""
Expand Down Expand Up @@ -4837,20 +4844,25 @@ def zrevrank(
name: KeyT,
value: EncodableT,
withscore: bool = False,
score_cast_func: Union[type, Callable] = float,
) -> ResponseT:
"""
Returns a 0-based value indicating the descending rank of
``value`` in sorted set ``name``.
The optional ``withscore`` argument supplements the command's
reply with the score of the element returned.

``score_cast_func`` a callable used to cast the score return value

For more information, see https://redis.io/commands/zrevrank
"""
pieces = ["ZREVRANK", name, value]
if withscore:
return self.execute_command(
"ZREVRANK", name, value, "WITHSCORE", keys=[name]
)
return self.execute_command("ZREVRANK", name, value, keys=[name])
pieces.append("WITHSCORE")

options = {"withscore": withscore, "score_cast_func": score_cast_func}

return self.execute_command(*pieces, **options)

def zscore(self, name: KeyT, value: EncodableT) -> ResponseT:
"""
Expand All @@ -4865,16 +4877,26 @@ def zunion(
keys: Union[Sequence[KeyT], Mapping[AnyKeyT, float]],
aggregate: Optional[str] = None,
withscores: bool = False,
score_cast_func: Union[type, Callable] = float,
) -> ResponseT:
"""
Return the union of multiple sorted sets specified by ``keys``.
``keys`` can be provided as dictionary of keys and their weights.
Scores will be aggregated based on the ``aggregate``, or SUM if
none is provided.

``score_cast_func`` a callable used to cast the score return value

For more information, see https://redis.io/commands/zunion
"""
return self._zaggregate("ZUNION", None, keys, aggregate, withscores=withscores)
return self._zaggregate(
"ZUNION",
None,
keys,
aggregate,
withscores=withscores,
score_cast_func=score_cast_func,
)

def zunionstore(
self,
Expand Down
44 changes: 37 additions & 7 deletions tests/test_asyncio/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from redis.commands.json.path import Path
from redis.commands.search.field import TextField
from redis.commands.search.query import Query
from redis.utils import safe_str
from tests.conftest import (
assert_resp_response,
assert_resp_response_in,
Expand Down Expand Up @@ -2071,11 +2072,14 @@ async def test_zrange(self, r: redis.Redis):
r, response, [(b"a2", 2.0), (b"a3", 3.0)], [[b"a2", 2.0], [b"a3", 3.0]]
)

# custom score function
# assert await r.zrange("a", 0, 1, withscores=True, score_cast_func=int) == [
# (b"a1", 1),
# (b"a2", 2),
# ]
# custom score cast function
response = await r.zrange("a", 0, 1, withscores=True, score_cast_func=safe_str)
assert_resp_response(
r,
response,
[(b"a1", "1"), (b"a2", "2")],
[[b"a1", "1.0"], [b"a2", "2.0"]],
)

@skip_if_server_version_lt("2.8.9")
async def test_zrangebylex(self, r: redis.Redis):
Expand Down Expand Up @@ -2127,6 +2131,15 @@ async def test_zrangebyscore(self, r: redis.Redis):
[(b"a2", 2), (b"a3", 3), (b"a4", 4)],
[[b"a2", 2], [b"a3", 3], [b"a4", 4]],
)
response = await r.zrangebyscore(
"a", 2, 4, withscores=True, score_cast_func=safe_str
)
assert_resp_response(
r,
response,
[(b"a2", "2"), (b"a3", "3"), (b"a4", "4")],
[[b"a2", "2.0"], [b"a3", "3.0"], [b"a4", "4.0"]],
)

async def test_zrank(self, r: redis.Redis):
await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
Expand All @@ -2141,10 +2154,14 @@ async def test_zrank_withscore(self, r: redis.Redis):
assert await r.zrank("a", "a2") == 1
assert await r.zrank("a", "a6") is None
assert_resp_response(
r, await r.zrank("a", "a3", withscore=True), [2, b"3"], [2, 3.0]
r, await r.zrank("a", "a3", withscore=True), [2, 3.0], [2, 3.0]
)
assert await r.zrank("a", "a6", withscore=True) is None

# custom score cast function
response = await r.zrank("a", "a3", withscore=True, score_cast_func=safe_str)
assert_resp_response(r, response, [2, "3"], [2, "3.0"])

async def test_zrem(self, r: redis.Redis):
await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
assert await r.zrem("a", "a2") == 1
Expand Down Expand Up @@ -2200,6 +2217,19 @@ async def test_zrevrange(self, r: redis.Redis):
r, response, [(b"a3", 3), (b"a2", 2)], [[b"a3", 3], [b"a2", 2]]
)

# custom score cast function
# should be applied to resp2 and resp3
# responses
response = await r.zrevrange(
"a", 0, 1, withscores=True, score_cast_func=safe_str
)
assert_resp_response(
r,
response,
[(b"a3", "3"), (b"a2", "2")],
[[b"a3", "3.0"], [b"a2", "2.0"]],
)

async def test_zrevrangebyscore(self, r: redis.Redis):
await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
assert await r.zrevrangebyscore("a", 4, 2) == [b"a4", b"a3", b"a2"]
Expand Down Expand Up @@ -2240,7 +2270,7 @@ async def test_zrevrank_withscore(self, r: redis.Redis):
assert await r.zrevrank("a", "a2") == 3
assert await r.zrevrank("a", "a6") is None
assert_resp_response(
r, await r.zrevrank("a", "a3", withscore=True), [2, b"3"], [2, 3.0]
r, await r.zrevrank("a", "a3", withscore=True), [2, 3.0], [2, 3.0]
)
assert await r.zrevrank("a", "a6", withscore=True) is None

Expand Down
70 changes: 57 additions & 13 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from redis.commands.json.path import Path
from redis.commands.search.field import TextField
from redis.commands.search.query import Query
from redis.utils import safe_str
from tests.test_utils import redis_server_time

from .conftest import (
Expand Down Expand Up @@ -3039,11 +3040,13 @@ def test_zrange(self, r):
[[b"a2", 2.0], [b"a3", 3.0]],
)

# # custom score function
# assert r.zrange("a", 0, 1, withscores=True, score_cast_func=int) == [
# (b"a1", 1),
# (b"a2", 2),
# ]
# custom score cast function
assert_resp_response(
r,
r.zrange("a", 0, 1, withscores=True, score_cast_func=safe_str),
[(b"a1", "1"), (b"a2", "2")],
[[b"a1", "1.0"], [b"a2", "2.0"]],
)

def test_zrange_errors(self, r):
with pytest.raises(exceptions.DataError):
Expand Down Expand Up @@ -3153,6 +3156,13 @@ def test_zrangebyscore(self, r):
[(b"a2", 2), (b"a3", 3), (b"a4", 4)],
[[b"a2", 2], [b"a3", 3], [b"a4", 4]],
)
# custom score cast function
assert_resp_response(
r,
r.zrangebyscore("a", 2, 4, withscores=True, score_cast_func=safe_str),
[(b"a2", "2"), (b"a3", "3"), (b"a4", "4")],
[[b"a2", "2.0"], [b"a3", "3.0"], [b"a4", "4.0"]],
)

def test_zrank(self, r):
r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
Expand All @@ -3166,9 +3176,17 @@ def test_zrank_withscore(self, r: redis.Redis):
assert r.zrank("a", "a1") == 0
assert r.zrank("a", "a2") == 1
assert r.zrank("a", "a6") is None
assert_resp_response(r, r.zrank("a", "a3", withscore=True), [2, b"3"], [2, 3.0])
assert_resp_response(r, r.zrank("a", "a3", withscore=True), [2, 3.0], [2, 3.0])
assert r.zrank("a", "a6", withscore=True) is None

# custom score cast function
assert_resp_response(
r,
r.zrank("a", "a3", withscore=True, score_cast_func=safe_str),
[2, "3"],
[2, "3.0"],
)

def test_zrem(self, r):
r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
assert r.zrem("a", "a2") == 1
Expand Down Expand Up @@ -3222,11 +3240,15 @@ def test_zrevrange(self, r):
[[b"a2", 2.0], [b"a1", 1.0]],
)

# # custom score function
# assert r.zrevrange("a", 0, 1, withscores=True, score_cast_func=int) == [
# (b"a3", 3.0),
# (b"a2", 2.0),
# ]
# custom score cast function
# should be applied to resp2 and resp3
# responses
assert_resp_response(
r,
r.zrevrange("a", 0, 1, withscores=True, score_cast_func=safe_str),
[(b"a3", "3"), (b"a2", "2")],
[[b"a3", "3.0"], [b"a2", "2.0"]],
)

def test_zrevrangebyscore(self, r):
r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
Expand All @@ -3241,13 +3263,20 @@ def test_zrevrangebyscore(self, r):
[(b"a4", 4.0), (b"a3", 3.0), (b"a2", 2.0)],
[[b"a4", 4.0], [b"a3", 3.0], [b"a2", 2.0]],
)
# custom score function
# custom score type cast function
assert_resp_response(
r,
r.zrevrangebyscore("a", 4, 2, withscores=True, score_cast_func=int),
[(b"a4", 4.0), (b"a3", 3.0), (b"a2", 2.0)],
[[b"a4", 4.0], [b"a3", 3.0], [b"a2", 2.0]],
)
# custom score cast function
assert_resp_response(
r,
r.zrevrangebyscore("a", 4, 2, withscores=True, score_cast_func=safe_str),
[(b"a4", "4"), (b"a3", "3"), (b"a2", "2")],
[[b"a4", "4.0"], [b"a3", "3.0"], [b"a2", "2.0"]],
)

def test_zrevrank(self, r):
r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
Expand All @@ -3262,10 +3291,18 @@ def test_zrevrank_withscore(self, r):
assert r.zrevrank("a", "a2") == 3
assert r.zrevrank("a", "a6") is None
assert_resp_response(
r, r.zrevrank("a", "a3", withscore=True), [2, b"3"], [2, 3.0]
r, r.zrevrank("a", "a3", withscore=True), [2, 3.0], [2, 3.0]
)
assert r.zrevrank("a", "a6", withscore=True) is None

# custom score cast function
assert_resp_response(
r,
r.zrevrank("a", "a3", withscore=True, score_cast_func=safe_str),
[2, "3"],
[2, "3.0"],
)

def test_zscore(self, r):
r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
assert r.zscore("a", "a1") == 1.0
Expand Down Expand Up @@ -3307,6 +3344,13 @@ def test_zunion(self, r):
[(b"a2", 5), (b"a4", 12), (b"a3", 20), (b"a1", 23)],
[[b"a2", 5], [b"a4", 12], [b"a3", 20], [b"a1", 23]],
)
# with custom score cast function
assert_resp_response(
r,
r.zunion(["a", "b", "c"], withscores=True, score_cast_func=safe_str),
[(b"a2", "3"), (b"a4", "4"), (b"a3", "8"), (b"a1", "9")],
[[b"a2", "3.0"], [b"a4", "4.0"], [b"a3", "8.0"], [b"a1", "9.0"]],
)

@pytest.mark.onlynoncluster
def test_zunionstore_sum(self, r):
Expand Down
Loading