Skip to content

Commit 5ee081d

Browse files
authored
Adding score_cast_func argument to zrank, zrevrank and zunion - for consistency with the other sorted sets commands (#3795)
* Adding score_cast_func argument to zrank, zrevrank and zunion - for consistency with the other sorted sets commands * Adding async tests; Add docstrings info for the new arg; removed unused option * Applying review comments * Try to fix the failing tests with Python 3.9 - it looks like a new dep version problem * Restricting some lib versions for tests with PyPy Python - with newer versions we have hanging multiprocessing tests * Restricting all test deps to specific versions for PyPy tests * Skipping multiprocessing tests with PyPY Python due to unstable behavior - sometimes processes hang and cause action timeouts. Enabling all long running pipeline jobs to be started after the initial fast checks. * Restricting lib requirements for Python 3.9 and fix an pubsub flaky test
1 parent 7f08087 commit 5ee081d

File tree

8 files changed

+208
-39
lines changed

8 files changed

+208
-39
lines changed

.github/workflows/integration.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494

9595
python-compatibility-tests:
9696
runs-on: ubuntu-latest
97-
needs: [ redis_version, tests ]
97+
needs: [ redis_version ]
9898
timeout-minutes: 60
9999
strategy:
100100
max-parallel: 15
@@ -118,7 +118,7 @@ jobs:
118118

119119
hiredis-tests:
120120
runs-on: ubuntu-latest
121-
needs: [redis_version, tests]
121+
needs: [redis_version]
122122
timeout-minutes: 60
123123
strategy:
124124
max-parallel: 15
@@ -144,7 +144,7 @@ jobs:
144144

145145
uvloop-tests:
146146
runs-on: ubuntu-latest
147-
needs: [redis_version, tests]
147+
needs: [redis_version]
148148
timeout-minutes: 60
149149
strategy:
150150
max-parallel: 15
@@ -170,7 +170,7 @@ jobs:
170170
build-and-test-package:
171171
name: Validate building and installing the package
172172
runs-on: ubuntu-latest
173-
needs: [tests]
173+
needs: [redis_version]
174174
strategy:
175175
fail-fast: false
176176
matrix:

dev_requirements.txt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
build
2+
build==1.2.2.post1 ; platform_python_implementation == "PyPy" or python_version < "3.10"
23
click==8.0.4
34
invoke==2.2.0
45
mock
6+
mock==5.1.0 ; platform_python_implementation == "PyPy" or python_version < "3.10"
57
packaging>=20.4
8+
packaging==24.2 ; platform_python_implementation == "PyPy" or python_version < "3.10"
9+
610
pytest
11+
pytest==8.3.4 ; platform_python_implementation == "PyPy" or python_version < "3.10"
712
pytest-asyncio>=0.23.0
13+
pytest-asyncio==1.1.0 ; platform_python_implementation == "PyPy" or python_version < "3.10"
814
pytest-cov
15+
pytest-cov==6.0.0 ; platform_python_implementation == "PyPy" or python_version < "3.10"
16+
coverage==7.6.12 ; platform_python_implementation == "PyPy" or python_version < "3.10"
917
pytest-profiling==1.8.1
1018
pytest-timeout
19+
pytest-timeout==2.3.1 ; platform_python_implementation == "PyPy" or python_version < "3.10"
20+
1121
ruff==0.9.6
1222
ujson>=4.2.0
13-
uvloop
23+
uvloop<=0.21.0; platform_python_implementation == "CPython"
1424
vulture>=2.3.0
15-
numpy>=1.24.0
25+
26+
numpy>=1.24.0 ; platform_python_implementation == "CPython"
27+
numpy>=1.24.0,<2.0 ; platform_python_implementation == "PyPy" or python_version < "3.10"
28+
1629
redis-entraid==1.0.0
1730
pybreaker>=1.4.0

redis/_parsers/helpers.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,39 @@ def zset_score_pairs(response, **options):
224224
return list(zip(it, map(score_cast_func, it)))
225225

226226

227+
def zset_score_for_rank(response, **options):
228+
"""
229+
If ``withscores`` is specified in the options, return the response as
230+
a [value, score] pair
231+
"""
232+
if not response or not options.get("withscore"):
233+
return response
234+
score_cast_func = options.get("score_cast_func", float)
235+
return [response[0], score_cast_func(response[1])]
236+
237+
238+
def zset_score_pairs_resp3(response, **options):
239+
"""
240+
If ``withscores`` is specified in the options, return the response as
241+
a list of [value, score] pairs
242+
"""
243+
if not response or not options.get("withscores"):
244+
return response
245+
score_cast_func = options.get("score_cast_func", float)
246+
return [[name, score_cast_func(val)] for name, val in response]
247+
248+
249+
def zset_score_for_rank_resp3(response, **options):
250+
"""
251+
If ``withscores`` is specified in the options, return the response as
252+
a [value, score] pair
253+
"""
254+
if not response or not options.get("withscore"):
255+
return response
256+
score_cast_func = options.get("score_cast_func", float)
257+
return [response[0], score_cast_func(response[1])]
258+
259+
227260
def sort_return_tuples(response, **options):
228261
"""
229262
If ``groups`` is specified, return the response as a list of
@@ -797,10 +830,14 @@ def string_keys_to_dict(key_string, callback):
797830
"SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
798831
),
799832
**string_keys_to_dict(
800-
"ZDIFF ZINTER ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYSCORE ZRANK ZREVRANGE "
801-
"ZREVRANGEBYSCORE ZREVRANK ZUNION",
833+
"ZDIFF ZINTER ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYSCORE ZREVRANGE "
834+
"ZREVRANGEBYSCORE ZUNION",
802835
zset_score_pairs,
803836
),
837+
**string_keys_to_dict(
838+
"ZREVRANK ZRANK",
839+
zset_score_for_rank,
840+
),
804841
**string_keys_to_dict("ZINCRBY ZSCORE", float_or_none),
805842
**string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True),
806843
**string_keys_to_dict("BLPOP BRPOP", lambda r: r and tuple(r) or None),
@@ -844,10 +881,17 @@ def string_keys_to_dict(key_string, callback):
844881
"SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
845882
),
846883
**string_keys_to_dict(
847-
"ZRANGE ZINTER ZPOPMAX ZPOPMIN ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE "
848-
"ZUNION HGETALL XREADGROUP",
884+
"ZRANGE ZINTER ZPOPMAX ZPOPMIN HGETALL XREADGROUP",
849885
lambda r, **kwargs: r,
850886
),
887+
**string_keys_to_dict(
888+
"ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
889+
zset_score_pairs_resp3,
890+
),
891+
**string_keys_to_dict(
892+
"ZREVRANK ZRANK",
893+
zset_score_for_rank_resp3,
894+
),
851895
**string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3),
852896
"ACL LOG": lambda r: (
853897
[

redis/commands/core.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4776,18 +4776,25 @@ def zrank(
47764776
name: KeyT,
47774777
value: EncodableT,
47784778
withscore: bool = False,
4779+
score_cast_func: Union[type, Callable] = float,
47794780
) -> ResponseT:
47804781
"""
47814782
Returns a 0-based value indicating the rank of ``value`` in sorted set
47824783
``name``.
47834784
The optional WITHSCORE argument supplements the command's
47844785
reply with the score of the element returned.
47854786
4787+
``score_cast_func`` a callable used to cast the score return value
4788+
47864789
For more information, see https://redis.io/commands/zrank
47874790
"""
4791+
pieces = ["ZRANK", name, value]
47884792
if withscore:
4789-
return self.execute_command("ZRANK", name, value, "WITHSCORE", keys=[name])
4790-
return self.execute_command("ZRANK", name, value, keys=[name])
4793+
pieces.append("WITHSCORE")
4794+
4795+
options = {"withscore": withscore, "score_cast_func": score_cast_func}
4796+
4797+
return self.execute_command(*pieces, **options)
47914798

47924799
def zrem(self, name: KeyT, *values: FieldT) -> ResponseT:
47934800
"""
@@ -4835,20 +4842,25 @@ def zrevrank(
48354842
name: KeyT,
48364843
value: EncodableT,
48374844
withscore: bool = False,
4845+
score_cast_func: Union[type, Callable] = float,
48384846
) -> ResponseT:
48394847
"""
48404848
Returns a 0-based value indicating the descending rank of
48414849
``value`` in sorted set ``name``.
48424850
The optional ``withscore`` argument supplements the command's
48434851
reply with the score of the element returned.
48444852
4853+
``score_cast_func`` a callable used to cast the score return value
4854+
48454855
For more information, see https://redis.io/commands/zrevrank
48464856
"""
4857+
pieces = ["ZREVRANK", name, value]
48474858
if withscore:
4848-
return self.execute_command(
4849-
"ZREVRANK", name, value, "WITHSCORE", keys=[name]
4850-
)
4851-
return self.execute_command("ZREVRANK", name, value, keys=[name])
4859+
pieces.append("WITHSCORE")
4860+
4861+
options = {"withscore": withscore, "score_cast_func": score_cast_func}
4862+
4863+
return self.execute_command(*pieces, **options)
48524864

48534865
def zscore(self, name: KeyT, value: EncodableT) -> ResponseT:
48544866
"""
@@ -4863,16 +4875,26 @@ def zunion(
48634875
keys: Union[Sequence[KeyT], Mapping[AnyKeyT, float]],
48644876
aggregate: Optional[str] = None,
48654877
withscores: bool = False,
4878+
score_cast_func: Union[type, Callable] = float,
48664879
) -> ResponseT:
48674880
"""
48684881
Return the union of multiple sorted sets specified by ``keys``.
48694882
``keys`` can be provided as dictionary of keys and their weights.
48704883
Scores will be aggregated based on the ``aggregate``, or SUM if
48714884
none is provided.
48724885
4886+
``score_cast_func`` a callable used to cast the score return value
4887+
48734888
For more information, see https://redis.io/commands/zunion
48744889
"""
4875-
return self._zaggregate("ZUNION", None, keys, aggregate, withscores=withscores)
4890+
return self._zaggregate(
4891+
"ZUNION",
4892+
None,
4893+
keys,
4894+
aggregate,
4895+
withscores=withscores,
4896+
score_cast_func=score_cast_func,
4897+
)
48764898

48774899
def zunionstore(
48784900
self,

tests/test_asyncio/test_commands.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from redis.commands.json.path import Path
2424
from redis.commands.search.field import TextField
2525
from redis.commands.search.query import Query
26+
from redis.utils import safe_str
2627
from tests.conftest import (
2728
assert_resp_response,
2829
assert_resp_response_in,
@@ -2071,11 +2072,14 @@ async def test_zrange(self, r: redis.Redis):
20712072
r, response, [(b"a2", 2.0), (b"a3", 3.0)], [[b"a2", 2.0], [b"a3", 3.0]]
20722073
)
20732074

2074-
# custom score function
2075-
# assert await r.zrange("a", 0, 1, withscores=True, score_cast_func=int) == [
2076-
# (b"a1", 1),
2077-
# (b"a2", 2),
2078-
# ]
2075+
# custom score cast function
2076+
response = await r.zrange("a", 0, 1, withscores=True, score_cast_func=safe_str)
2077+
assert_resp_response(
2078+
r,
2079+
response,
2080+
[(b"a1", "1"), (b"a2", "2")],
2081+
[[b"a1", "1.0"], [b"a2", "2.0"]],
2082+
)
20792083

20802084
@skip_if_server_version_lt("2.8.9")
20812085
async def test_zrangebylex(self, r: redis.Redis):
@@ -2127,6 +2131,15 @@ async def test_zrangebyscore(self, r: redis.Redis):
21272131
[(b"a2", 2), (b"a3", 3), (b"a4", 4)],
21282132
[[b"a2", 2], [b"a3", 3], [b"a4", 4]],
21292133
)
2134+
response = await r.zrangebyscore(
2135+
"a", 2, 4, withscores=True, score_cast_func=safe_str
2136+
)
2137+
assert_resp_response(
2138+
r,
2139+
response,
2140+
[(b"a2", "2"), (b"a3", "3"), (b"a4", "4")],
2141+
[[b"a2", "2.0"], [b"a3", "3.0"], [b"a4", "4.0"]],
2142+
)
21302143

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

2161+
# custom score cast function
2162+
response = await r.zrank("a", "a3", withscore=True, score_cast_func=safe_str)
2163+
assert_resp_response(r, response, [2, "3"], [2, "3.0"])
2164+
21482165
async def test_zrem(self, r: redis.Redis):
21492166
await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
21502167
assert await r.zrem("a", "a2") == 1
@@ -2200,6 +2217,19 @@ async def test_zrevrange(self, r: redis.Redis):
22002217
r, response, [(b"a3", 3), (b"a2", 2)], [[b"a3", 3], [b"a2", 2]]
22012218
)
22022219

2220+
# custom score cast function
2221+
# should be applied to resp2 and resp3
2222+
# responses
2223+
response = await r.zrevrange(
2224+
"a", 0, 1, withscores=True, score_cast_func=safe_str
2225+
)
2226+
assert_resp_response(
2227+
r,
2228+
response,
2229+
[(b"a3", "3"), (b"a2", "2")],
2230+
[[b"a3", "3.0"], [b"a2", "2.0"]],
2231+
)
2232+
22032233
async def test_zrevrangebyscore(self, r: redis.Redis):
22042234
await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
22052235
assert await r.zrevrangebyscore("a", 4, 2) == [b"a4", b"a3", b"a2"]
@@ -2240,7 +2270,7 @@ async def test_zrevrank_withscore(self, r: redis.Redis):
22402270
assert await r.zrevrank("a", "a2") == 3
22412271
assert await r.zrevrank("a", "a6") is None
22422272
assert_resp_response(
2243-
r, await r.zrevrank("a", "a3", withscore=True), [2, b"3"], [2, 3.0]
2273+
r, await r.zrevrank("a", "a3", withscore=True), [2, 3.0], [2, 3.0]
22442274
)
22452275
assert await r.zrevrank("a", "a6", withscore=True) is None
22462276

0 commit comments

Comments
 (0)