diff options
| -rw-r--r-- | redis/asyncio/connection.py | 2 | ||||
| -rw-r--r-- | redis/cluster.py | 2 | ||||
| -rw-r--r-- | redis/parsers/commands.py | 185 | ||||
| -rw-r--r-- | redis/parsers/encoders.py | 4 | ||||
| -rw-r--r-- | redis/parsers/hiredis.py | 11 | ||||
| -rw-r--r-- | redis/parsers/protocol.py | 12 | ||||
| -rw-r--r-- | redis/parsers/resp2.py | 2 | ||||
| -rw-r--r-- | redis/utils.py | 2 | ||||
| -rw-r--r-- | tests/test_encoding.py | 2 |
9 files changed, 109 insertions, 113 deletions
diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py index 6275632..4faee34 100644 --- a/redis/asyncio/connection.py +++ b/redis/asyncio/connection.py @@ -40,7 +40,7 @@ from redis.exceptions import ( ResponseError, TimeoutError, ) -from redis.typing import EncodableT, EncodedT +from redis.typing import EncodableT from redis.utils import HIREDIS_AVAILABLE, str_if_bytes from ..parsers import AsyncHiredisParser, AsyncRESP2Parser, BaseParser, Encoder diff --git a/redis/cluster.py b/redis/cluster.py index 245dfdb..3fcae73 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -9,7 +9,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union from redis.backoff import default_backoff from redis.client import CaseInsensitiveDict, PubSub, Redis, parse_scan from redis.commands import READ_COMMANDS, RedisClusterCommands -from redis.connection import ConnectionPool, DefaultParser, Encoder, parse_url +from redis.connection import ConnectionPool, DefaultParser, parse_url from redis.crc import REDIS_CLUSTER_HASH_SLOTS, key_slot from redis.exceptions import ( AskError, diff --git a/redis/parsers/commands.py b/redis/parsers/commands.py index 7981a52..73c45ba 100644 --- a/redis/parsers/commands.py +++ b/redis/parsers/commands.py @@ -7,94 +7,6 @@ if TYPE_CHECKING: from redis.asyncio.cluster import ClusterNode -class AsyncCommandsParser: - """ - Parses Redis commands to get command keys. - - COMMAND output is used to determine key locations. - Commands that do not have a predefined key location are flagged with 'movablekeys', - and these commands' keys are determined by the command 'COMMAND GETKEYS'. - - NOTE: Due to a bug in redis<7.0, this does not work properly - for EVAL or EVALSHA when the `numkeys` arg is 0. - - issue: https://github.com/redis/redis/issues/9493 - - fix: https://github.com/redis/redis/pull/9733 - - So, don't use this with EVAL or EVALSHA. - """ - - __slots__ = ("commands", "node") - - def __init__(self) -> None: - self.commands: Dict[str, Union[int, Dict[str, Any]]] = {} - - async def initialize(self, node: Optional["ClusterNode"] = None) -> None: - if node: - self.node = node - - commands = await self.node.execute_command("COMMAND") - for cmd, command in commands.items(): - if "movablekeys" in command["flags"]: - commands[cmd] = -1 - elif command["first_key_pos"] == 0 and command["last_key_pos"] == 0: - commands[cmd] = 0 - elif command["first_key_pos"] == 1 and command["last_key_pos"] == 1: - commands[cmd] = 1 - self.commands = {cmd.upper(): command for cmd, command in commands.items()} - - # As soon as this PR is merged into Redis, we should reimplement - # our logic to use COMMAND INFO changes to determine the key positions - # https://github.com/redis/redis/pull/8324 - async def get_keys(self, *args: Any) -> Optional[Tuple[str, ...]]: - if len(args) < 2: - # The command has no keys in it - return None - - try: - command = self.commands[args[0]] - except KeyError: - # try to split the command name and to take only the main command - # e.g. 'memory' for 'memory usage' - args = args[0].split() + list(args[1:]) - cmd_name = args[0].upper() - if cmd_name not in self.commands: - # We'll try to reinitialize the commands cache, if the engine - # version has changed, the commands may not be current - await self.initialize() - if cmd_name not in self.commands: - raise RedisError( - f"{cmd_name} command doesn't exist in Redis commands" - ) - - command = self.commands[cmd_name] - - if command == 1: - return (args[1],) - if command == 0: - return None - if command == -1: - return await self._get_moveable_keys(*args) - - last_key_pos = command["last_key_pos"] - if last_key_pos < 0: - last_key_pos = len(args) + last_key_pos - return args[command["first_key_pos"] : last_key_pos + 1 : command["step_count"]] - - async def _get_moveable_keys(self, *args: Any) -> Optional[Tuple[str, ...]]: - try: - keys = await self.node.execute_command("COMMAND GETKEYS", *args) - except ResponseError as e: - message = e.__str__() - if ( - "Invalid arguments" in message - or "The command has no key arguments" in message - ): - return None - else: - raise e - return keys - - class CommandsParser: """ Parses Redis commands to get command keys. @@ -109,7 +21,7 @@ class CommandsParser: self.initialize(redis_connection) def initialize(self, r): - commands = r.execute_command("COMMAND") + commands = r.command() uppercase_commands = [] for cmd in commands: if any(x.isupper() for x in cmd): @@ -210,14 +122,11 @@ class CommandsParser: So, don't use this function with EVAL or EVALSHA. """ - pieces = [] - cmd_name = args[0] # The command name should be splitted into separate arguments, # e.g. 'MEMORY USAGE' will be splitted into ['MEMORY', 'USAGE'] - pieces = pieces + cmd_name.split() - pieces = pieces + list(args[1:]) + pieces = args[0].split() + list(args[1:]) try: - keys = redis_conn.execute_command("COMMAND GETKEYS", *pieces) + keys = redis_conn.command_getkeys(*pieces) except ResponseError as e: message = e.__str__() if ( @@ -257,3 +166,91 @@ class CommandsParser: # PUBLISH channel message keys = [args[1]] return keys + + +class AsyncCommandsParser: + """ + Parses Redis commands to get command keys. + + COMMAND output is used to determine key locations. + Commands that do not have a predefined key location are flagged with 'movablekeys', + and these commands' keys are determined by the command 'COMMAND GETKEYS'. + + NOTE: Due to a bug in redis<7.0, this does not work properly + for EVAL or EVALSHA when the `numkeys` arg is 0. + - issue: https://github.com/redis/redis/issues/9493 + - fix: https://github.com/redis/redis/pull/9733 + + So, don't use this with EVAL or EVALSHA. + """ + + __slots__ = ("commands", "node") + + def __init__(self) -> None: + self.commands: Dict[str, Union[int, Dict[str, Any]]] = {} + + async def initialize(self, node: Optional["ClusterNode"] = None) -> None: + if node: + self.node = node + + commands = await self.node.command() + for cmd, command in commands.items(): + if "movablekeys" in command["flags"]: + commands[cmd] = -1 + elif command["first_key_pos"] == 0 and command["last_key_pos"] == 0: + commands[cmd] = 0 + elif command["first_key_pos"] == 1 and command["last_key_pos"] == 1: + commands[cmd] = 1 + self.commands = {cmd.upper(): command for cmd, command in commands.items()} + + # As soon as this PR is merged into Redis, we should reimplement + # our logic to use COMMAND INFO changes to determine the key positions + # https://github.com/redis/redis/pull/8324 + async def get_keys(self, *args: Any) -> Optional[Tuple[str, ...]]: + if len(args) < 2: + # The command has no keys in it + return None + + try: + command = self.commands[args[0]] + except KeyError: + # try to split the command name and to take only the main command + # e.g. 'memory' for 'memory usage' + args = args[0].split() + list(args[1:]) + cmd_name = args[0].upper() + if cmd_name not in self.commands: + # We'll try to reinitialize the commands cache, if the engine + # version has changed, the commands may not be current + await self.initialize() + if cmd_name not in self.commands: + raise RedisError( + f"{cmd_name} command doesn't exist in Redis commands" + ) + + command = self.commands[cmd_name] + + if command == 1: + return (args[1],) + if command == 0: + return None + if command == -1: + return await self._get_moveable_keys(*args) + + last_key_pos = command["last_key_pos"] + if last_key_pos < 0: + last_key_pos = len(args) + last_key_pos + return args[command["first_key_pos"] : last_key_pos + 1 : command["step_count"]] + + async def _get_moveable_keys(self, *args: Any) -> Optional[Tuple[str, ...]]: + try: + keys = await self.node.command_getkeys(*args) + except ResponseError as e: + message = e.__str__() + if ( + "Invalid arguments" in message + or "The command has no key arguments" in message + ): + return None + else: + raise e + return keys diff --git a/redis/parsers/encoders.py b/redis/parsers/encoders.py index 85fb292..60e8b1b 100644 --- a/redis/parsers/encoders.py +++ b/redis/parsers/encoders.py @@ -1,12 +1,10 @@ -from redis.typing import EncodableT, EncodedT - from ..exceptions import DataError class Encoder: "Encode strings to bytes-like and decode bytes-like to strings" - def __init__(self, encoding, encoding_errors, decode_responses): + def __init__(self, encoding, encoding_errors, decode_responses: bool): self.encoding = encoding self.encoding_errors = encoding_errors self.decode_responses = decode_responses diff --git a/redis/parsers/hiredis.py b/redis/parsers/hiredis.py index 44d5b81..93f05ba 100644 --- a/redis/parsers/hiredis.py +++ b/redis/parsers/hiredis.py @@ -148,12 +148,10 @@ class AsyncHiredisParser(AsyncBaseParser): if not HIREDIS_AVAILABLE: raise RedisError("Hiredis is not available.") - import hiredis - super().__init__(socket_read_size=socket_read_size) - self._reader: Optional[hiredis.Reader] = None + self._reader = None - def on_connect(self, connection: "Connection"): + def on_connect(self, connection): import hiredis self._stream = connection._reader @@ -201,7 +199,10 @@ class AsyncHiredisParser(AsyncBaseParser): response = self._reader.gets() while response is False: await self.read_from_socket() - response = self._reader.gets() + if disable_decoding: + response = self._reader.gets(False) + else: + response = self._reader.gets() # if the response is a ConnectionError or the response is a list and # the first item is a ConnectionError, raise it as something bad diff --git a/redis/parsers/protocol.py b/redis/parsers/protocol.py index 6faaddd..22c89ba 100644 --- a/redis/parsers/protocol.py +++ b/redis/parsers/protocol.py @@ -76,6 +76,12 @@ class BaseParser(ABC): return exception_class(response) return ResponseError(response) + def on_disconnect(self): + raise NotImplementedError() + + def on_connect(self, connection): + raise NotImplementedError() + class AsyncBaseParser(BaseParser): """Base parsing class for the python-backed async parser""" @@ -92,12 +98,6 @@ class AsyncBaseParser(BaseParser): except Exception: pass - def on_disconnect(self): - raise NotImplementedError() - - def on_connect(self, connection: "Connection"): - raise NotImplementedError() - async def can_read_destructive(self) -> bool: raise NotImplementedError() diff --git a/redis/parsers/resp2.py b/redis/parsers/resp2.py index 32872ea..9a47040 100644 --- a/redis/parsers/resp2.py +++ b/redis/parsers/resp2.py @@ -103,7 +103,7 @@ class AsyncRESP2Parser(AsyncBaseParser): super().__init__(socket_read_size) self.encoder: Optional[Encoder] = None - def on_connect(self, connection: "Connection"): + def on_connect(self, connection): """Called when the stream connects""" self._stream = connection._reader if self._stream is None: diff --git a/redis/utils.py b/redis/utils.py index fa2d407..1171ed0 100644 --- a/redis/utils.py +++ b/redis/utils.py @@ -11,7 +11,7 @@ except ImportError: HIREDIS_AVAILABLE = False try: - import ssl + import ssl # noqa SSL_AVAILABLE = True except ImportError: diff --git a/tests/test_encoding.py b/tests/test_encoding.py index ee52e4c..e1e33f8 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -97,7 +97,7 @@ class TestCommandsAreNotEncoded: class TestInvalidUserInput: - + def test_none_fails(self, r): with pytest.raises(redis.DataError): r.set("a", None) |
