Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 6 additions & 9 deletions docs/supported-databases/valkey.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
Valkey
======

Integration with `Valkey <https://valkey.io//>`_ using the `Valkey Docker Image <https://hub.docker.com/_/valkey>`_
Integration with `Valkey <https://valkey.io/>`_ using the `Valkey Docker Image <https://hub.docker.com/_/valkey>`_.

Installation
------------

.. code-block:: bash

pip install pytest-databases[valkey]
pip install pytest-databases[valkey] valkey

The ``valkey`` Python client is no longer pulled by ``pytest-databases[valkey]`` — the fixture validates the container via the bundled ``valkey-cli`` invoked through ``container.exec_run`` — so install your own client alongside ``pytest-databases``.

Usage Example
-------------
Expand All @@ -25,23 +27,18 @@ Usage Example
client = Valkey(
host=valkey_service.host,
port=valkey_service.port,
db=valkey_service.db
db=valkey_service.db,
)
client.set("test_key", "test_value")
assert client.get("test_key") == b"test_value"

def test(valkey_connection: Valkey) -> None:
valkey_connection.set("test_key", "test_value")
assert valkey_connection.get("test_key") == b"test_value"

Available Fixtures
------------------

* ``valkey_port``: The port number for the Valkey service.
* ``valkey_host``: The host name for the Valkey service.
* ``valkey_image``: The Docker image to use for Valkey.
* ``valkey_service``: A fixture that provides a Valkey service.
* ``valkey_connection``: A fixture that provides a Valkey connection.
* ``valkey_service``: A fixture that provides a ``ValkeyService`` (``host``, ``port``, ``container``, ``db``).

Service API
-----------
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ oracle = ["oracledb"]
postgres = ["psycopg>=3"]
redis = ["redis"]
spanner = ["google-cloud-spanner"]
valkey = ["valkey"]
valkey = []
yugabyte = []

[dependency-groups]
Expand Down Expand Up @@ -385,7 +385,9 @@ line-length = 120

[tool.pytest.ini_options]
addopts = "--doctest-glob='*.md' --dist=loadgroup"
cdist-group-steal = "3:10"
# Keep the middle shard non-empty when file justification moves a large test file
# across cdist chunk boundaries. pytest-cdist 0.3.x accepts only one steal entry.
cdist-group-steal = "2:10"
cdist-justify-items = "file"
filterwarnings = [
"ignore::DeprecationWarning:pkg_resources",
Expand Down
46 changes: 31 additions & 15 deletions src/pytest_databases/docker/valkey.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
from __future__ import annotations

import dataclasses
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING

import pytest
from valkey import Valkey
from valkey.exceptions import ConnectionError as ValkeyConnectionError

from pytest_databases.helpers import get_xdist_worker_num
from pytest_databases.types import ServiceContainer, XdistIsolationLevel

if TYPE_CHECKING:
from collections.abc import Generator
from collections.abc import Generator, Iterator

from docker.models.containers import Container

from pytest_databases._service import DockerService


def _output_to_bytes(output: bytes | str | Iterator[bytes]) -> bytes:
if isinstance(output, bytes):
return output
if isinstance(output, str):
return output.encode()
return b"".join(output)


def _exec_valkey_cli(container: Container, *args: str, db: int = 0) -> tuple[int, bytes]:
result = container.exec_run([
"valkey-cli",
"-h",
"localhost",
"-p",
"6379",
"-n",
str(db),
*args,
])
return result.exit_code if result.exit_code is not None else -1, _output_to_bytes(result.output)


@dataclasses.dataclass
class ValkeyService(ServiceContainer):
db: int
Expand All @@ -26,16 +48,6 @@ def xdist_valkey_isolation_level() -> XdistIsolationLevel:
return "database"


def valkey_responsive(service_container: ServiceContainer) -> bool:
client = Valkey(host=service_container.host, port=service_container.port)
try:
return cast("bool", client.ping())
except (ConnectionError, ValkeyConnectionError):
return False
finally:
client.close()


@pytest.fixture(autouse=False, scope="session")
def valkey_port(valkey_service: ValkeyService) -> int:
return valkey_service.port
Expand Down Expand Up @@ -68,9 +80,13 @@ def valkey_service(
else:
name += f"_{worker_num + 1}"

def _responsive(_service: ServiceContainer) -> bool:
exit_code, output = _exec_valkey_cli(_service.container, "PING")
return exit_code == 0 and output.strip().endswith(b"PONG")

with docker_service.run(
valkey_image,
check=valkey_responsive,
check=_responsive,
container_port=6379,
name=name,
transient=xdist_valkey_isolation_level == "server",
Expand Down
90 changes: 63 additions & 27 deletions tests/test_valkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,90 @@
import pytest


def test_plugin_imports_without_valkey(pytester: pytest.Pytester) -> None:
pytester.makepyfile("""
import builtins

def test_import() -> None:
original_import = builtins.__import__

def blocked_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "valkey" or name.startswith("valkey."):
raise ModuleNotFoundError(name)
return original_import(name, globals, locals, fromlist, level)

builtins.__import__ = blocked_import
try:
import pytest_databases.docker.valkey
finally:
builtins.__import__ = original_import
""")

result = pytester.runpytest_subprocess("-p", "pytest_databases", "-vv")
result.assert_outcomes(passed=1)


VALKEY_TEST_HELPERS = """
def run_valkey(service, *args, db=None):
if db is None:
db = service.db
result = service.container.exec_run([
"valkey-cli",
"-h",
"localhost",
"-p",
"6379",
"-n",
str(db),
*args,
])
assert result.exit_code == 0, result.output.decode(errors="replace")
output = result.output
if isinstance(output, bytes):
return output.decode().strip()
if isinstance(output, str):
return output.strip()
return b"".join(output).decode().strip()
"""


@pytest.fixture(params=[pytest.param("valkey_service", id="valkey")])
def valkey_compatible_service(request: pytest.FixtureRequest) -> str:
return request.param


def test_default_no_xdist(pytester: pytest.Pytester, valkey_compatible_service: str) -> None:
pytester.makepyfile(f"""
import pytest
import valkey
from pytest_databases.docker.valkey import ValkeyService
from pytest_databases.helpers import get_xdist_worker_num

pytest_plugins = [
"pytest_databases.docker.valkey",
]
pytest_plugins = ["pytest_databases.docker.valkey"]

{VALKEY_TEST_HELPERS}

def test_valkey_service({valkey_compatible_service}: ValkeyService) -> None:
assert valkey.Valkey.from_url("valkey://", host={valkey_compatible_service}.host, port={valkey_compatible_service}.port).ping()
assert run_valkey({valkey_compatible_service}, "PING") == "PONG"
""")
result = pytester.runpytest_subprocess("-p", "pytest_databases")
result.assert_outcomes(passed=1)


def test_xdist_isolate_database(pytester: pytest.Pytester, valkey_compatible_service: str) -> None:
pytester.makepyfile(f"""
import pytest
import valkey
from pytest_databases.docker.valkey import ValkeyService
from pytest_databases.helpers import get_xdist_worker_num

pytest_plugins = [
"pytest_databases.docker.valkey",
]
pytest_plugins = ["pytest_databases.docker.valkey"]

{VALKEY_TEST_HELPERS}

def test_one({valkey_compatible_service}: ValkeyService) -> None:
client = valkey.Valkey.from_url("valkey://", host={valkey_compatible_service}.host, port={valkey_compatible_service}.port)
assert client.ping()
assert run_valkey({valkey_compatible_service}, "PING") == "PONG"
assert {valkey_compatible_service}.db == get_xdist_worker_num()


def test_two({valkey_compatible_service}: ValkeyService) -> None:
client = valkey.Valkey.from_url("valkey://", host={valkey_compatible_service}.host, port={valkey_compatible_service}.port)
assert not client.get("one")
client.set("one", "1")
assert run_valkey({valkey_compatible_service}, "GET", "one") in ("", "(nil)")
assert run_valkey({valkey_compatible_service}, "SET", "one", "1") == "OK"
assert {valkey_compatible_service}.db == get_xdist_worker_num()
""")
result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2")
Expand All @@ -56,29 +96,25 @@ def test_two({valkey_compatible_service}: ValkeyService) -> None:
def test_xdist_isolate_server(pytester: pytest.Pytester, valkey_compatible_service: str) -> None:
pytester.makepyfile(f"""
import pytest
import valkey
from pytest_databases.docker.valkey import ValkeyService
from pytest_databases.helpers import get_xdist_worker_num

pytest_plugins = [
"pytest_databases.docker.valkey",
]
pytest_plugins = ["pytest_databases.docker.valkey"]


@pytest.fixture(scope="session")
def xdist_valkey_isolation_level():
return "server"

{VALKEY_TEST_HELPERS}

def test_one({valkey_compatible_service}: ValkeyService) -> None:
client = valkey.Valkey.from_url("valkey://", host={valkey_compatible_service}.host, port={valkey_compatible_service}.port)
assert client.ping()
assert run_valkey({valkey_compatible_service}, "PING") == "PONG"
assert {valkey_compatible_service}.db == 0


def test_two({valkey_compatible_service}: ValkeyService) -> None:
client = valkey.Valkey.from_url("valkey://", host={valkey_compatible_service}.host, port={valkey_compatible_service}.port)
assert not client.get("one")
client.set("one", "1")
assert run_valkey({valkey_compatible_service}, "GET", "one") in ("", "(nil)")
assert run_valkey({valkey_compatible_service}, "SET", "one", "1") == "OK"
assert {valkey_compatible_service}.db == 0
""")
result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2")
Expand Down
18 changes: 1 addition & 17 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading