From cd92428cb3f0bc989bdf2fc03ee572bb239e7f3a Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 9 May 2024 10:11:59 +0300 Subject: [PATCH] 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 --- redis/_parsers/helpers.py | 7 ++++++- redis/commands/core.py | 32 +++++++++++++++++++++++------ tests/test_asyncio/test_commands.py | 27 ++++++++++++++++++++++++ tests/test_commands.py | 25 ++++++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/redis/_parsers/helpers.py b/redis/_parsers/helpers.py index a1df927bf..74faa5fd7 100644 --- a/redis/_parsers/helpers.py +++ b/redis/_parsers/helpers.py @@ -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 [] + else: + payload = r and pairs_to_dict(r) or {} + return int(cursor), payload def parse_zscan(response, **options): diff --git a/redis/commands/core.py b/redis/commands/core.py index 331c2e5e4..45ccf5be4 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -3106,6 +3106,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 @@ -3115,6 +3116,8 @@ 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] @@ -3122,13 +3125,16 @@ def hscan( 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 @@ -3137,11 +3143,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, @@ -3257,6 +3270,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 @@ -3265,14 +3279,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, diff --git a/tests/test_asyncio/test_commands.py b/tests/test_asyncio/test_commands.py index 7102450fe..b3e42bae8 100644 --- a/tests/test_asyncio/test_commands.py +++ b/tests/test_asyncio/test_commands.py @@ -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): @@ -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): diff --git a/tests/test_commands.py b/tests/test_commands.py index 1b5fe80e0..cd392552d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -2171,6 +2171,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): @@ -2179,6 +2192,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):