Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support NOVALUES parameter for HSCAN #3157

Merged
merged 4 commits into from May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion redis/_parsers/helpers.py
Expand Up @@ -354,7 +354,12 @@ def parse_scan(response, **options):

def parse_hscan(response, **options):
cursor, r = response
return int(cursor), r and pairs_to_dict(r) or {}
no_values = options.get("no_values", False)
if no_values:
payload = r or []
gerzse marked this conversation as resolved.
Show resolved Hide resolved
else:
payload = r and pairs_to_dict(r) or {}
return int(cursor), payload


def parse_zscan(response, **options):
Expand Down
32 changes: 26 additions & 6 deletions redis/commands/core.py
Expand Up @@ -3102,6 +3102,7 @@ def hscan(
cursor: int = 0,
match: Union[PatternT, None] = None,
count: Union[int, None] = None,
no_values: Union[bool, None] = None,
) -> ResponseT:
"""
Incrementally return key/value slices in a hash. Also return a cursor
Expand All @@ -3111,20 +3112,25 @@ def hscan(

``count`` allows for hint the minimum number of returns

``no_values`` indicates to return only the keys, without values.

For more information see https://redis.io/commands/hscan
"""
pieces: list[EncodableT] = [name, cursor]
if match is not None:
pieces.extend([b"MATCH", match])
if count is not None:
pieces.extend([b"COUNT", count])
return self.execute_command("HSCAN", *pieces)
if no_values is not None:
pieces.extend([b"NOVALUES"])
return self.execute_command("HSCAN", *pieces, no_values=no_values)

def hscan_iter(
self,
name: str,
match: Union[PatternT, None] = None,
count: Union[int, None] = None,
no_values: Union[bool, None] = None,
) -> Iterator:
"""
Make an iterator using the HSCAN command so that the client doesn't
Expand All @@ -3133,11 +3139,18 @@ def hscan_iter(
``match`` allows for filtering the keys by pattern

``count`` allows for hint the minimum number of returns

``no_values`` indicates to return only the keys, without values
"""
cursor = "0"
while cursor != 0:
cursor, data = self.hscan(name, cursor=cursor, match=match, count=count)
yield from data.items()
cursor, data = self.hscan(
name, cursor=cursor, match=match, count=count, no_values=no_values
)
if no_values:
yield from data
else:
yield from data.items()

def zscan(
self,
Expand Down Expand Up @@ -3253,6 +3266,7 @@ async def hscan_iter(
name: str,
match: Union[PatternT, None] = None,
count: Union[int, None] = None,
no_values: Union[bool, None] = None,
) -> AsyncIterator:
"""
Make an iterator using the HSCAN command so that the client doesn't
Expand All @@ -3261,14 +3275,20 @@ async def hscan_iter(
``match`` allows for filtering the keys by pattern

``count`` allows for hint the minimum number of returns

``no_values`` indicates to return only the keys, without values
"""
cursor = "0"
while cursor != 0:
cursor, data = await self.hscan(
name, cursor=cursor, match=match, count=count
name, cursor=cursor, match=match, count=count, no_values=no_values
)
for it in data.items():
yield it
if no_values:
for it in data:
yield it
else:
for it in data.items():
yield it

async def zscan_iter(
self,
Expand Down
27 changes: 27 additions & 0 deletions tests/test_asyncio/test_commands.py
Expand Up @@ -1349,6 +1349,19 @@ async def test_hscan(self, r: redis.Redis):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
_, dic = await r.hscan("a", match="a")
assert dic == {b"a": b"1"}
_, dic = await r.hscan("a_notset", match="a")
assert dic == {}

@skip_if_server_version_lt("7.4.0")
async def test_hscan_novalues(self, r: redis.Redis):
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
cursor, keys = await r.hscan("a", no_values=True)
assert cursor == 0
assert sorted(keys) == [b"a", b"b", b"c"]
_, keys = await r.hscan("a", match="a", no_values=True)
assert keys == [b"a"]
_, keys = await r.hscan("a_notset", match="a", no_values=True)
assert keys == []

@skip_if_server_version_lt("2.8.0")
async def test_hscan_iter(self, r: redis.Redis):
Expand All @@ -1357,6 +1370,20 @@ async def test_hscan_iter(self, r: redis.Redis):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
dic = {k: v async for k, v in r.hscan_iter("a", match="a")}
assert dic == {b"a": b"1"}
dic = {k: v async for k, v in r.hscan_iter("a_notset", match="a")}
assert dic == {}

@skip_if_server_version_lt("7.4.0")
async def test_hscan_iter_novalues(self, r: redis.Redis):
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
keys = list([k async for k in r.hscan_iter("a", no_values=True)])
assert sorted(keys) == [b"a", b"b", b"c"]
keys = list([k async for k in r.hscan_iter("a", match="a", no_values=True)])
assert keys == [b"a"]
keys = list(
[k async for k in r.hscan_iter("a", match="a_notset", no_values=True)]
)
assert keys == []

@skip_if_server_version_lt("2.8.0")
async def test_zscan(self, r: redis.Redis):
Expand Down
25 changes: 25 additions & 0 deletions tests/test_commands.py
Expand Up @@ -2162,6 +2162,19 @@ def test_hscan(self, r):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
_, dic = r.hscan("a", match="a")
assert dic == {b"a": b"1"}
_, dic = r.hscan("a_notset")
assert dic == {}

@skip_if_server_version_lt("7.4.0")
def test_hscan_novalues(self, r):
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
cursor, keys = r.hscan("a", no_values=True)
assert cursor == 0
assert sorted(keys) == [b"a", b"b", b"c"]
_, keys = r.hscan("a", match="a", no_values=True)
assert keys == [b"a"]
_, keys = r.hscan("a_notset", no_values=True)
assert keys == []

@skip_if_server_version_lt("2.8.0")
def test_hscan_iter(self, r):
Expand All @@ -2170,6 +2183,18 @@ def test_hscan_iter(self, r):
assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
dic = dict(r.hscan_iter("a", match="a"))
assert dic == {b"a": b"1"}
dic = dict(r.hscan_iter("a_notset"))
assert dic == {}

@skip_if_server_version_lt("7.4.0")
def test_hscan_iter_novalues(self, r):
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
keys = list(r.hscan_iter("a", no_values=True))
assert keys == [b"a", b"b", b"c"]
keys = list(r.hscan_iter("a", match="a", no_values=True))
assert keys == [b"a"]
keys = list(r.hscan_iter("a_notset", no_values=True))
assert keys == []

@skip_if_server_version_lt("2.8.0")
def test_zscan(self, r):
Expand Down