Skip to content

adjust cache signal receiver to work with both sync and async backends #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions django_valkey/async_cache/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ class AsyncValkeyCache(
BaseValkeyCache[AsyncDefaultClient, AValkey], AsyncBackendCommands
):
DEFAULT_CLIENT_CLASS = "django_valkey.async_cache.client.default.AsyncDefaultClient"
is_async = True
18 changes: 18 additions & 0 deletions django_valkey/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,21 @@ async def hkeys(self, *args, **kwargs) -> list[Any]:

async def hexists(self, *args, **kwargs) -> bool:
return await self.client.hexists(*args, **kwargs)


# temp fix for django's #36047
# TODO: remove this when it's fixed in django
from django.core import signals # noqa: E402
from django.core.cache import caches, close_caches # noqa: E402


async def close_async_caches(**kwargs):
for conn in caches.all(initialized_only=True):
if getattr(conn, "is_async", False):
await conn.aclose()
else:
conn.close()


signals.request_finished.connect(close_async_caches)
signals.request_finished.disconnect(close_caches)
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ brotli = [

[dependency-groups]
dev = [
"anyio>=4.9.0",
"black>=25.1.0",
"coverage>=7.8.0",
"django-cmd>=2.6",
Expand All @@ -66,7 +67,6 @@ dev = [
"mypy>=1.15.0",
"pre-commit>=4.2.0",
"pytest>=8.3.5",
"pytest-asyncio>=0.26.0",
"pytest-django>=4.11.1",
"pytest-mock>=3.14.0",
"pytest-subtests>=0.14.1",
Expand Down Expand Up @@ -104,8 +104,6 @@ ignore_missing_settings = true

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "tests.settings.sqlite"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"

[tool.coverage.run]
plugins = ["django_coverage_plugin"]
Expand Down
7 changes: 4 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from typing import cast

import pytest
import pytest_asyncio
from pytest_django.fixtures import SettingsWrapper

from asgiref.compatibility import iscoroutinefunction
Expand All @@ -12,10 +11,12 @@
from django_valkey.base import BaseValkeyCache
from django_valkey.cache import ValkeyCache

# for some reason `isawaitable` doesn't work here

pytestmark = pytest.mark.anyio

if iscoroutinefunction(default_cache.clear):

@pytest_asyncio.fixture(loop_scope="session")
@pytest.fixture(scope="function")
async def cache():
yield default_cache
await default_cache.aclear()
Expand Down
12 changes: 12 additions & 0 deletions tests/tests_async/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest


@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"


# this keeps the event loop open for the entire test suite
@pytest.fixture(scope="session", autouse=True)
async def keepalive(anyio_backend):
pass
26 changes: 14 additions & 12 deletions tests/tests_async/test_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import contextlib
import datetime
import threading
Expand All @@ -8,10 +7,11 @@
from unittest.mock import patch, AsyncMock

import pytest
import pytest_asyncio
from pytest_django.fixtures import SettingsWrapper
from pytest_mock import MockerFixture

import anyio

from django.core.cache import caches
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.test import override_settings
Expand All @@ -22,7 +22,10 @@
from django_valkey.serializers.msgpack import MSGPackSerializer


@pytest_asyncio.fixture(loop_scope="session")
pytestmark = pytest.mark.anyio


@pytest.fixture
async def patch_itersize_setting() -> Iterable[None]:
del caches["default"]
with override_settings(DJANGO_VALKEY_SCAN_ITERSIZE=30):
Expand All @@ -31,7 +34,6 @@ async def patch_itersize_setting() -> Iterable[None]:
del caches["default"]


@pytest.mark.asyncio(loop_scope="session")
class TestAsyncDjangoValkeyCache:
async def test_set_int(self, cache: AsyncValkeyCache):
if isinstance(cache.client, AsyncHerdClient):
Expand Down Expand Up @@ -72,15 +74,15 @@ async def test_setnx_timeout(self, cache: AsyncValkeyCache):
# test that timeout still works for nx=True
res = await cache.aset("test_key_nx", 1, timeout=2, nx=True)
assert res is True
await asyncio.sleep(3)
await anyio.sleep(3)
res = await cache.aget("test_key_nx")
assert res is None

# test that timeout will not affect key, if it was there
await cache.aset("test_key_nx", 1)
res = await cache.aset("test_key_nx", 2, timeout=2, nx=True)
assert res is None
await asyncio.sleep(3)
await anyio.sleep(3)
res = await cache.aget("test_key_nx")
assert res == 1

Expand Down Expand Up @@ -151,7 +153,7 @@ async def test_save_float(self, cache: AsyncValkeyCache):

async def test_timeout(self, cache: AsyncValkeyCache):
await cache.aset("test_key", 222, timeout=3)
await asyncio.sleep(4)
await anyio.sleep(4)

res = await cache.aget("test_key")
assert res is None
Expand All @@ -170,7 +172,7 @@ async def test_timeout_parameter_as_positional_argument(

await cache.aset("test_key", 222, 1)
res1 = await cache.aget("test_key")
await asyncio.sleep(2)
await anyio.sleep(2)
res2 = await cache.aget("test_key")
assert res1 == 222
assert res2 is None
Expand Down Expand Up @@ -731,7 +733,7 @@ async def test_lock_released_by_thread(self, cache: AsyncValkeyCache):
async def release_lock(lock_):
await lock_.release()

t = threading.Thread(target=asyncio.run, args=[release_lock(lock)])
t = threading.Thread(target=anyio.run, args=[release_lock, lock])
t.start()
t.join()

Expand Down Expand Up @@ -801,7 +803,7 @@ async def test_touch_positive_timeout(self, cache: AsyncValkeyCache):

assert await cache.atouch("test_key", 2) is True
assert await cache.aget("test_key") == 222
await asyncio.sleep(3)
await anyio.sleep(3)
assert await cache.aget("test_key") is None

async def test_touch_negative_timeout(self, cache: AsyncValkeyCache):
Expand All @@ -819,7 +821,7 @@ async def test_touch_forever(self, cache: AsyncValkeyCache):
result = await cache.atouch("test_key", None)
assert result is True
assert await cache.attl("test_key") is None
await asyncio.sleep(2)
await anyio.sleep(2)
assert await cache.aget("test_key") == "foo"

async def test_touch_forever_nonexistent(self, cache: AsyncValkeyCache):
Expand All @@ -830,7 +832,7 @@ async def test_touch_default_timeout(self, cache: AsyncValkeyCache):
await cache.aset("test_key", "foo", timeout=1)
result = await cache.atouch("test_key")
assert result is True
await asyncio.sleep(2)
await anyio.sleep(2)
assert await cache.aget("test_key") == "foo"

async def test_clear(self, cache: AsyncValkeyCache):
Expand Down
23 changes: 14 additions & 9 deletions tests/tests_async/test_cache_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import cast

import pytest
import pytest_asyncio
from pytest import LogCaptureFixture
from pytest_django.fixtures import SettingsWrapper

Expand All @@ -15,6 +14,8 @@
from django_valkey.async_cache.cache import AsyncValkeyCache
from django_valkey.async_cache.client import AsyncHerdClient, AsyncDefaultClient

pytestmark = pytest.mark.anyio

methods_with_no_parameters = {"clear", "close"}

methods_with_one_required_parameters = {
Expand Down Expand Up @@ -75,15 +76,19 @@
}


@pytest.mark.asyncio(loop_scope="session")
# TODO: when django adjusts the signal, remove this decorator (and the ones below)
@pytest.mark.filterwarnings("ignore:coroutine 'AsyncBackendCommands.close'")
class TestDjangoValkeyOmitException:
@pytest_asyncio.fixture
@pytest.fixture
async def conf_cache(self, settings: SettingsWrapper):
caches_settings = copy.deepcopy(settings.CACHES)
# NOTE: this files raises RuntimeWarning because `conn.close` was not awaited,
# this is expected because django calls the signal manually during this test
# to debug, put a `raise` in django.utils.connection.BaseConnectionHandler.close_all
settings.CACHES = caches_settings
return caches_settings

@pytest_asyncio.fixture
@pytest.fixture
async def conf_cache_to_ignore_exception(
self, settings: SettingsWrapper, conf_cache
):
Expand All @@ -92,7 +97,7 @@ async def conf_cache_to_ignore_exception(
settings.DJANGO_VALKEY_IGNORE_EXCEPTIONS = True
settings.DJANGO_VALKEY_LOG_IGNORE_EXCEPTIONS = True

@pytest_asyncio.fixture
@pytest.fixture
async def ignore_exceptions_cache(
self, conf_cache_to_ignore_exception
) -> AsyncValkeyCache:
Expand Down Expand Up @@ -210,7 +215,7 @@ async def test_error_raised_when_ignore_is_not_set(self, conf_cache):
await cache.get("key")


@pytest_asyncio.fixture
@pytest.fixture
async def key_prefix_cache(
cache: AsyncValkeyCache, settings: SettingsWrapper
) -> Iterable[AsyncValkeyCache]:
Expand All @@ -220,14 +225,14 @@ async def key_prefix_cache(
yield cache


@pytest_asyncio.fixture
@pytest.fixture
async def with_prefix_cache() -> Iterable[AsyncValkeyCache]:
with_prefix = cast(AsyncValkeyCache, caches["with_prefix"])
yield with_prefix
await with_prefix.clear()


@pytest.mark.asyncio(loop_scope="session")
@pytest.mark.filterwarnings("ignore:coroutine 'AsyncBackendCommands.close'")
class TestDjangoValkeyCacheEscapePrefix:
async def test_delete_pattern(
self, key_prefix_cache: AsyncValkeyCache, with_prefix_cache: AsyncValkeyCache
Expand Down Expand Up @@ -258,7 +263,7 @@ async def test_keys(
assert "b" not in keys


@pytest.mark.asyncio(loop_scope="session")
@pytest.mark.filterwarnings("ignore:coroutine 'AsyncBackendCommands.close'")
async def test_custom_key_function(cache: AsyncValkeyCache, settings: SettingsWrapper):
caches_setting = copy.deepcopy(settings.CACHES)
caches_setting["default"]["KEY_FUNCTION"] = "tests.test_cache_options.make_key"
Expand Down
6 changes: 3 additions & 3 deletions tests/tests_async/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from unittest.mock import AsyncMock

import pytest
import pytest_asyncio
from pytest_django.fixtures import SettingsWrapper
from pytest_mock import MockerFixture

Expand All @@ -11,16 +10,17 @@
from django_valkey.async_cache.cache import AsyncValkeyCache
from django_valkey.async_cache.client import AsyncDefaultClient

pytestmark = pytest.mark.anyio

@pytest_asyncio.fixture

@pytest.fixture
async def cache_client(cache: AsyncValkeyCache) -> Iterable[AsyncDefaultClient]:
client = cache.client
await client.aset("TestClientClose", 0)
yield client
await client.adelete("TestClientClose")


@pytest.mark.asyncio(loop_scope="session")
class TestClientClose:
async def test_close_client_disconnect_default(
self, cache_client: AsyncDefaultClient, mocker: MockerFixture
Expand Down
7 changes: 3 additions & 4 deletions tests/tests_async/test_connection_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from django_valkey.async_cache import pool


@pytest.mark.asyncio
pytestmark = pytest.mark.anyio


async def test_connection_factory_redefine_from_opts():
cf = sync_pool.get_connection_factory(
options={
Expand All @@ -30,7 +32,6 @@ async def test_connection_factory_redefine_from_opts():
),
],
)
@pytest.mark.asyncio
async def test_connection_factory_opts(conn_factory: str, expected):
cf = sync_pool.get_connection_factory(
path=None,
Expand All @@ -55,7 +56,6 @@ async def test_connection_factory_opts(conn_factory: str, expected):
),
],
)
@pytest.mark.asyncio
async def test_connection_factory_path(conn_factory: str, expected):
cf = sync_pool.get_connection_factory(
path=conn_factory,
Expand All @@ -66,7 +66,6 @@ async def test_connection_factory_path(conn_factory: str, expected):
assert isinstance(cf, expected)


@pytest.mark.asyncio
async def test_connection_factory_no_sentinels():
with pytest.raises(ImproperlyConfigured):
sync_pool.get_connection_factory(
Expand Down
3 changes: 2 additions & 1 deletion tests/tests_async/test_connection_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from django_valkey import pool

pytestmark = pytest.mark.anyio


@pytest.mark.parametrize(
"connection_string",
Expand All @@ -11,7 +13,6 @@
"valkeys://localhost:3333?db=2",
],
)
@pytest.mark.asyncio
async def test_connection_strings(connection_string: str):
cf = pool.get_connection_factory(
options={
Expand Down
Loading
Loading