Skip to content

Commit 3f8491c

Browse files
committed
feat: move Redis family to clientless validation
1 parent 4c84394 commit 3f8491c

5 files changed

Lines changed: 173 additions & 243 deletions

File tree

docs/supported-databases/redis.rst

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
Redis
22
=====
33

4-
Integration with `Redis <https://redis.io/>`_ using the `Redis Docker Image <https://hub.docker.com/_/redis>`_, Snap's `Key DB<https://docs.keydb.dev/>` or `Dragonfly <https://www.dragonflydb.io/>`_.
4+
Integration with `Redis <https://redis.io/>`_ using the `Redis Docker Image <https://hub.docker.com/_/redis>`_, `KeyDB <https://docs.keydb.dev/>`_, or `Dragonfly <https://www.dragonflydb.io/>`_. KeyDB and Dragonfly are wire-compatible with Redis, so a ``redis.Redis`` client works against all three services.
55

66
Installation
77
------------
88

99
.. code-block:: bash
1010
11-
pip install pytest-databases[redis]
11+
pip install pytest-databases[redis] redis
12+
13+
The ``redis`` package is no longer pulled by the ``pytest-databases[redis]`` extra — the fixtures validate the container via ``redis-cli`` invoked from a short-lived sidecar — so install your own client (``redis``, ``redis[hiredis]``, etc.) alongside ``pytest-databases``.
1214

1315
Usage Example
1416
-------------
@@ -21,32 +23,37 @@ Usage Example
2123
2224
pytest_plugins = ["pytest_databases.docker.redis"]
2325
24-
def test(redis_service: RedisService) -> None:
26+
def test_redis(redis_service: RedisService) -> None:
2527
client = redis.Redis(
2628
host=redis_service.host,
2729
port=redis_service.port,
28-
db=redis_service.db
30+
db=redis_service.db,
2931
)
3032
client.set("test_key", "test_value")
3133
assert client.get("test_key") == b"test_value"
3234
33-
def test(redis_connection: redis.Redis) -> None:
34-
redis_connection.set("test_key", "test_value")
35-
assert redis_connection.get("test_key") == b"test_value"
35+
def test_keydb(keydb_service: RedisService) -> None:
36+
client = redis.Redis(host=keydb_service.host, port=keydb_service.port, db=keydb_service.db)
37+
client.set("test_key", "test_value")
38+
assert client.get("test_key") == b"test_value"
39+
40+
def test_dragonfly(dragonfly_service: RedisService) -> None:
41+
client = redis.Redis(host=dragonfly_service.host, port=dragonfly_service.port, db=dragonfly_service.db)
42+
client.set("test_key", "test_value")
43+
assert client.get("test_key") == b"test_value"
3644
3745
Available Fixtures
3846
------------------
3947

4048
* ``redis_port``: The port number for the Redis service.
4149
* ``redis_host``: The host name for the Redis service.
4250
* ``redis_image``: The Docker image to use for Redis.
43-
* ``redis_service``: A fixture that provides a Redis service.
44-
* ``redis_connection``: A fixture that provides a Redis connection.
51+
* ``redis_service``: A fixture that provides a ``RedisService`` (``host``, ``port``, ``container``, ``db``).
4552

46-
The following version-specific fixtures are also available:
53+
The following compatible-service fixtures are also available:
4754

48-
* ``dragonflydb_port``, ``dragonflydb_host``, ``dragonflydb_image``, ``dragonflydb_service``, ``dragonflydb_connection``: Latest Available DragonflyDB Docker image.
49-
* ``keydb_port``, ``keydb_host``, ``keydb_image``, ``keydb_service``, ``keydb_connection``: Latest Available KeyDB Docker image.
55+
* ``dragonfly_port``, ``dragonfly_host``, ``dragonfly_image``, ``dragonfly_service``: Latest available DragonflyDB Docker image.
56+
* ``keydb_port``, ``keydb_host``, ``keydb_image``, ``keydb_service``: Latest available KeyDB Docker image.
5057

5158
Service API
5259
-----------

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,18 @@ Source = "https://github.com/litestar-org/pytest-databases"
6262
azure-storage = ["azure-storage-blob"]
6363
bigquery = ["google-cloud-bigquery"]
6464
cockroachdb = []
65-
dragonfly = ["redis"]
65+
dragonfly = []
6666
elasticsearch7 = []
6767
elasticsearch8 = []
6868
gizmosql = ["adbc-driver-flightsql", "pyarrow"]
69-
keydb = ["redis"]
69+
keydb = []
7070
mariadb = []
7171
mongodb = []
7272
mssql = []
7373
mysql = []
7474
oracle = []
7575
postgres = ["psycopg>=3"]
76-
redis = ["redis"]
76+
redis = []
7777
spanner = ["google-cloud-spanner"]
7878
valkey = []
7979
yugabyte = []
@@ -115,7 +115,6 @@ lint = [
115115
"types-decorator",
116116
"types-pyyaml",
117117
"types-docutils",
118-
"types-redis",
119118
"types-pymysql",
120119
"slotscheck",
121120
]

src/pytest_databases/docker/redis.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,72 @@
44
from typing import TYPE_CHECKING
55

66
import pytest
7-
from redis import Redis
8-
from redis.exceptions import ConnectionError as RedisConnectionError
7+
from docker.errors import ContainerError
98

109
from pytest_databases.helpers import get_xdist_worker_num
1110
from pytest_databases.types import ServiceContainer, XdistIsolationLevel
1211

1312
if TYPE_CHECKING:
14-
from collections.abc import Generator
13+
from collections.abc import Generator, Iterator
14+
15+
from docker.models.containers import Container
1516

1617
from pytest_databases._service import DockerService
1718

1819

20+
REDIS_PROBE_IMAGE = "redis:latest"
21+
22+
23+
def _output_to_bytes(output: bytes | str | Iterator[bytes]) -> bytes:
24+
if isinstance(output, bytes):
25+
return output
26+
if isinstance(output, str):
27+
return output.encode()
28+
return b"".join(output)
29+
30+
31+
def _exec_redis_cli(container: Container, *args: str, db: int = 0) -> tuple[int, bytes]:
32+
result = container.exec_run([
33+
"redis-cli",
34+
"-h",
35+
"localhost",
36+
"-p",
37+
"6379",
38+
"-n",
39+
str(db),
40+
*args,
41+
])
42+
return result.exit_code if result.exit_code is not None else -1, _output_to_bytes(result.output)
43+
44+
45+
def _probe_redis_endpoint(
46+
docker_service: DockerService,
47+
service: ServiceContainer,
48+
*args: str,
49+
db: int = 0,
50+
probe_image: str = REDIS_PROBE_IMAGE,
51+
) -> tuple[int, bytes]:
52+
try:
53+
output = docker_service._client.containers.run(
54+
image=probe_image,
55+
command=[
56+
"redis-cli",
57+
"-h",
58+
service.host,
59+
"-p",
60+
str(service.port),
61+
"-n",
62+
str(db),
63+
*args,
64+
],
65+
network_mode="host",
66+
remove=True,
67+
)
68+
except ContainerError as exc:
69+
return exc.exit_status, _output_to_bytes(exc.stderr) if exc.stderr else str(exc).encode()
70+
return 0, _output_to_bytes(output)
71+
72+
1973
@dataclasses.dataclass
2074
class RedisService(ServiceContainer):
2175
db: int
@@ -26,16 +80,6 @@ def xdist_redis_isolation_level() -> XdistIsolationLevel:
2680
return "database"
2781

2882

29-
def redis_responsive(service_container: ServiceContainer) -> bool:
30-
client = Redis(host=service_container.host, port=service_container.port)
31-
try:
32-
return client.ping()
33-
except (ConnectionError, RedisConnectionError):
34-
return False
35-
finally:
36-
client.close()
37-
38-
3983
@pytest.fixture(autouse=False, scope="session")
4084
def redis_port(redis_service: RedisService) -> int:
4185
return redis_service.port
@@ -66,9 +110,13 @@ def redis_service(
66110
else:
67111
name += f"_{worker_num + 1}"
68112

113+
def _responsive(_service: ServiceContainer) -> bool:
114+
exit_code, output = _probe_redis_endpoint(docker_service, _service, "PING")
115+
return exit_code == 0 and output.strip().endswith(b"PONG")
116+
69117
with docker_service.run(
70118
redis_image,
71-
check=redis_responsive,
119+
check=_responsive,
72120
container_port=6379,
73121
name=name,
74122
transient=xdist_redis_isolation_level == "server",
@@ -101,9 +149,13 @@ def dragonfly_service(
101149
else:
102150
name += f"_{worker_num + 1}"
103151

152+
def _responsive(_service: ServiceContainer) -> bool:
153+
exit_code, output = _probe_redis_endpoint(docker_service, _service, "PING")
154+
return exit_code == 0 and output.strip().endswith(b"PONG")
155+
104156
with docker_service.run(
105157
dragonfly_image,
106-
check=redis_responsive,
158+
check=_responsive,
107159
container_port=6379,
108160
name=name,
109161
transient=xdist_redis_isolation_level == "server",
@@ -146,9 +198,13 @@ def keydb_service(
146198
else:
147199
name += f"_{worker_num + 1}"
148200

201+
def _responsive(_service: ServiceContainer) -> bool:
202+
exit_code, output = _probe_redis_endpoint(docker_service, _service, "PING")
203+
return exit_code == 0 and output.strip().endswith(b"PONG")
204+
149205
with docker_service.run(
150206
keydb_image,
151-
check=redis_responsive,
207+
check=_responsive,
152208
container_port=6379,
153209
name=name,
154210
transient=xdist_redis_isolation_level == "server",

0 commit comments

Comments
 (0)