Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions redis/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
_AsyncRESP3Parser,
)

if HIREDIS_AVAILABLE:
import hiredis

SYM_STAR = b"*"
SYM_DOLLAR = b"$"
SYM_CRLF = b"\r\n"
Expand All @@ -130,6 +133,26 @@
DefaultParser = _AsyncRESP3Parser


class HiredisRespSerializer:
def __init__(self, fallback):
self._fallback = fallback

def pack(self, *args: EncodableT) -> List[bytes]:
"""Pack a series of arguments into the Redis protocol"""
if any(isinstance(arg, memoryview) for arg in args):
return self._fallback(*args)
if isinstance(args[0], str):
args = tuple(args[0].encode().split()) + args[1:]
elif b" " in args[0]:
args = tuple(args[0].split()) + args[1:]
args = tuple(bytes(arg) if isinstance(arg, bytearray) else arg for arg in args)
try:
return [hiredis.pack_command(args)]
except TypeError:
_, value, traceback = sys.exc_info()
raise DataError(value).with_traceback(traceback)


class ConnectCallbackProtocol(Protocol):
def __call__(self, connection: "AbstractConnection"): ...

Expand Down Expand Up @@ -689,6 +712,11 @@ def __init__(
self._connect_callbacks: List[weakref.WeakMethod[ConnectCallbackT]] = []
self._buffer_cutoff = 6000
self._re_auth_token: Optional[TokenInterface] = None
self._command_packer = (
HiredisRespSerializer(self._pack_command_python)
if HIREDIS_AVAILABLE
else None
)
self._should_reconnect = False

try:
Expand Down Expand Up @@ -1316,6 +1344,11 @@ async def _read_response_from_parser(
return await self._parser.read_response(disable_decoding=disable_decoding)

def pack_command(self, *args: EncodableT) -> List[bytes]:
if self._command_packer is not None:
return self._command_packer.pack(*args)
return self._pack_command_python(*args)

def _pack_command_python(self, *args: EncodableT) -> List[bytes]:
"""Pack a series of arguments into the Redis protocol"""
output = []
# the client might have included 1 or more literal arguments in
Expand Down
25 changes: 25 additions & 0 deletions tests/test_asyncio/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from redis.asyncio import ConnectionPool, Redis
from redis.asyncio.connection import (
Connection,
HiredisRespSerializer,
SSLConnection,
UnixDomainSocketConnection,
parse_url,
Expand Down Expand Up @@ -83,6 +84,30 @@ def test_connection_default_parser_matches_default_protocol():
assert conn.protocol == 3


@pytest.mark.skipif(not HIREDIS_AVAILABLE, reason="hiredis is not installed")
def test_connection_uses_hiredis_command_packer(monkeypatch):
calls = []

def pack_command(args):
calls.append(args)
return b"packed"

monkeypatch.setattr("redis.asyncio.connection.hiredis.pack_command", pack_command)
connection = Connection()

assert isinstance(connection._command_packer, HiredisRespSerializer)
assert connection.pack_command("SET", "key", "value") == [b"packed"]
assert calls == [(b"SET", "key", "value")]


def test_connection_uses_python_command_packer_without_hiredis(monkeypatch):
monkeypatch.setattr("redis.asyncio.connection.HIREDIS_AVAILABLE", False)
connection = Connection()

assert connection._command_packer is None
assert connection.pack_command("PING") == [b"*1\r\n$4\r\nPING\r\n"]


@pytest.mark.parametrize(
("buffer", "eof", "expected"),
[
Expand Down