From 7beaaea07eb095171a5c7579951774ef40bfbacd Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 21 Feb 2024 13:49:38 +0200 Subject: [PATCH 1/3] Support NOVALUES parameter for HSCAN Issue #3153 The NOVALUES parameter instructs HSCAN to only return the hash keys, without values. --- redis/_parsers/helpers.py | 7 ++++++- redis/commands/core.py | 25 +++++++++++++++++++++---- tests/test_commands.py | 25 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/redis/_parsers/helpers.py b/redis/_parsers/helpers.py index bdd749a5bc..dc4726b898 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 464e8d8c85..d25a8a47e4 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -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 @@ -3111,6 +3112,9 @@ def hscan( ``count`` allows for hint the minimum number of returns + ``no_values`` indicates to return only the keys, without values. The + return type in this case is a list of keys. + For more information see https://redis.io/commands/hscan """ pieces: list[EncodableT] = [name, cursor] @@ -3118,13 +3122,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 @@ -3133,11 +3140,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, @@ -3253,6 +3267,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 @@ -3261,11 +3276,13 @@ 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 diff --git a/tests/test_commands.py b/tests/test_commands.py index b2d7c1b9ed..2aca07cd5c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -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 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): @@ -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): From 79c72a57dc5ab5545c6b8cf7e2cebb106dbf5995 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Mon, 29 Apr 2024 14:06:47 +0300 Subject: [PATCH 2/3] Add tests for async --- redis/commands/core.py | 11 +++++++---- tests/test_asyncio/test_commands.py | 25 +++++++++++++++++++++++++ tests/test_commands.py | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/redis/commands/core.py b/redis/commands/core.py index ca5f909263..b9fff2a414 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -3112,8 +3112,7 @@ def hscan( ``count`` allows for hint the minimum number of returns - ``no_values`` indicates to return only the keys, without values. The - return type in this case is a list of keys. + ``no_values`` indicates to return only the keys, without values. For more information see https://redis.io/commands/hscan """ @@ -3284,8 +3283,12 @@ async def hscan_iter( cursor, data = await self.hscan( 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 7102450fe4..84f11e1c45 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,18 @@ 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 2aca07cd5c..2e22a27136 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -2170,7 +2170,7 @@ 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 keys == [b"a", b"b", b"c"] + 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) From 632cc61e0a85fa7d61068c8d35409113c7fca808 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Mon, 29 Apr 2024 14:19:15 +0300 Subject: [PATCH 3/3] Fix linter errors --- tests/test_asyncio/test_commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_asyncio/test_commands.py b/tests/test_asyncio/test_commands.py index 84f11e1c45..b3e42bae8e 100644 --- a/tests/test_asyncio/test_commands.py +++ b/tests/test_asyncio/test_commands.py @@ -1380,7 +1380,9 @@ async def test_hscan_iter_novalues(self, r: redis.Redis): 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)]) + 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")