Skip to content

Commit 6b7c292

Browse files
committed
Support NOVALUES parameter for HSCAN (#3157)
* Support NOVALUES parameter for HSCAN Issue #3153 The NOVALUES parameter instructs HSCAN to only return the hash keys, without values. Co-authored-by: Gabriel Erzse <[email protected]>
1 parent 2820e87 commit 6b7c292

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed

redis/_parsers/helpers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,12 @@ def parse_scan(response, **options):
361361

362362
def parse_hscan(response, **options):
363363
cursor, r = response
364-
return int(cursor), r and pairs_to_dict(r) or {}
364+
no_values = options.get("no_values", False)
365+
if no_values:
366+
payload = r or []
367+
else:
368+
payload = r and pairs_to_dict(r) or {}
369+
return int(cursor), payload
365370

366371

367372
def parse_zscan(response, **options):

redis/commands/core.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3106,6 +3106,7 @@ def hscan(
31063106
cursor: int = 0,
31073107
match: Union[PatternT, None] = None,
31083108
count: Union[int, None] = None,
3109+
no_values: Union[bool, None] = None,
31093110
) -> ResponseT:
31103111
"""
31113112
Incrementally return key/value slices in a hash. Also return a cursor
@@ -3115,20 +3116,25 @@ def hscan(
31153116
31163117
``count`` allows for hint the minimum number of returns
31173118
3119+
``no_values`` indicates to return only the keys, without values.
3120+
31183121
For more information see https://redis.io/commands/hscan
31193122
"""
31203123
pieces: list[EncodableT] = [name, cursor]
31213124
if match is not None:
31223125
pieces.extend([b"MATCH", match])
31233126
if count is not None:
31243127
pieces.extend([b"COUNT", count])
3125-
return self.execute_command("HSCAN", *pieces)
3128+
if no_values is not None:
3129+
pieces.extend([b"NOVALUES"])
3130+
return self.execute_command("HSCAN", *pieces, no_values=no_values)
31263131

31273132
def hscan_iter(
31283133
self,
31293134
name: str,
31303135
match: Union[PatternT, None] = None,
31313136
count: Union[int, None] = None,
3137+
no_values: Union[bool, None] = None,
31323138
) -> Iterator:
31333139
"""
31343140
Make an iterator using the HSCAN command so that the client doesn't
@@ -3137,11 +3143,18 @@ def hscan_iter(
31373143
``match`` allows for filtering the keys by pattern
31383144
31393145
``count`` allows for hint the minimum number of returns
3146+
3147+
``no_values`` indicates to return only the keys, without values
31403148
"""
31413149
cursor = "0"
31423150
while cursor != 0:
3143-
cursor, data = self.hscan(name, cursor=cursor, match=match, count=count)
3144-
yield from data.items()
3151+
cursor, data = self.hscan(
3152+
name, cursor=cursor, match=match, count=count, no_values=no_values
3153+
)
3154+
if no_values:
3155+
yield from data
3156+
else:
3157+
yield from data.items()
31453158

31463159
def zscan(
31473160
self,
@@ -3257,6 +3270,7 @@ async def hscan_iter(
32573270
name: str,
32583271
match: Union[PatternT, None] = None,
32593272
count: Union[int, None] = None,
3273+
no_values: Union[bool, None] = None,
32603274
) -> AsyncIterator:
32613275
"""
32623276
Make an iterator using the HSCAN command so that the client doesn't
@@ -3265,14 +3279,20 @@ async def hscan_iter(
32653279
``match`` allows for filtering the keys by pattern
32663280
32673281
``count`` allows for hint the minimum number of returns
3282+
3283+
``no_values`` indicates to return only the keys, without values
32683284
"""
32693285
cursor = "0"
32703286
while cursor != 0:
32713287
cursor, data = await self.hscan(
3272-
name, cursor=cursor, match=match, count=count
3288+
name, cursor=cursor, match=match, count=count, no_values=no_values
32733289
)
3274-
for it in data.items():
3275-
yield it
3290+
if no_values:
3291+
for it in data:
3292+
yield it
3293+
else:
3294+
for it in data.items():
3295+
yield it
32763296

32773297
async def zscan_iter(
32783298
self,

tests/test_asyncio/test_commands.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,19 @@ async def test_hscan(self, r: redis.Redis):
13481348
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
13491349
_, dic = await r.hscan("a", match="a")
13501350
assert dic == {b"a": b"1"}
1351+
_, dic = await r.hscan("a_notset", match="a")
1352+
assert dic == {}
1353+
1354+
@skip_if_server_version_lt("7.4.0")
1355+
async def test_hscan_novalues(self, r: redis.Redis):
1356+
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
1357+
cursor, keys = await r.hscan("a", no_values=True)
1358+
assert cursor == 0
1359+
assert sorted(keys) == [b"a", b"b", b"c"]
1360+
_, keys = await r.hscan("a", match="a", no_values=True)
1361+
assert keys == [b"a"]
1362+
_, keys = await r.hscan("a_notset", match="a", no_values=True)
1363+
assert keys == []
13511364

13521365
@skip_if_server_version_lt("2.8.0")
13531366
async def test_hscan_iter(self, r: redis.Redis):
@@ -1356,6 +1369,20 @@ async def test_hscan_iter(self, r: redis.Redis):
13561369
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
13571370
dic = {k: v async for k, v in r.hscan_iter("a", match="a")}
13581371
assert dic == {b"a": b"1"}
1372+
dic = {k: v async for k, v in r.hscan_iter("a_notset", match="a")}
1373+
assert dic == {}
1374+
1375+
@skip_if_server_version_lt("7.4.0")
1376+
async def test_hscan_iter_novalues(self, r: redis.Redis):
1377+
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
1378+
keys = list([k async for k in r.hscan_iter("a", no_values=True)])
1379+
assert sorted(keys) == [b"a", b"b", b"c"]
1380+
keys = list([k async for k in r.hscan_iter("a", match="a", no_values=True)])
1381+
assert keys == [b"a"]
1382+
keys = list(
1383+
[k async for k in r.hscan_iter("a", match="a_notset", no_values=True)]
1384+
)
1385+
assert keys == []
13591386

13601387
@skip_if_server_version_lt("2.8.0")
13611388
async def test_zscan(self, r: redis.Redis):

tests/test_commands.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,6 +2171,19 @@ def test_hscan(self, r):
21712171
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
21722172
_, dic = r.hscan("a", match="a")
21732173
assert dic == {b"a": b"1"}
2174+
_, dic = r.hscan("a_notset")
2175+
assert dic == {}
2176+
2177+
@skip_if_server_version_lt("7.4.0")
2178+
def test_hscan_novalues(self, r):
2179+
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
2180+
cursor, keys = r.hscan("a", no_values=True)
2181+
assert cursor == 0
2182+
assert sorted(keys) == [b"a", b"b", b"c"]
2183+
_, keys = r.hscan("a", match="a", no_values=True)
2184+
assert keys == [b"a"]
2185+
_, keys = r.hscan("a_notset", no_values=True)
2186+
assert keys == []
21742187

21752188
@skip_if_server_version_lt("2.8.0")
21762189
def test_hscan_iter(self, r):
@@ -2179,6 +2192,18 @@ def test_hscan_iter(self, r):
21792192
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
21802193
dic = dict(r.hscan_iter("a", match="a"))
21812194
assert dic == {b"a": b"1"}
2195+
dic = dict(r.hscan_iter("a_notset"))
2196+
assert dic == {}
2197+
2198+
@skip_if_server_version_lt("7.4.0")
2199+
def test_hscan_iter_novalues(self, r):
2200+
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
2201+
keys = list(r.hscan_iter("a", no_values=True))
2202+
assert keys == [b"a", b"b", b"c"]
2203+
keys = list(r.hscan_iter("a", match="a", no_values=True))
2204+
assert keys == [b"a"]
2205+
keys = list(r.hscan_iter("a_notset", no_values=True))
2206+
assert keys == []
21822207

21832208
@skip_if_server_version_lt("2.8.0")
21842209
def test_zscan(self, r):

0 commit comments

Comments
 (0)