Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ jobs:
uv run pytest tests/*.py --ds=tests.settings.sqlite -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_herd -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_json -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_msgspec_json -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_msgspec_msgpack -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_msgpack -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_sentinel -x
uv run pytest tests/*.py --ds=tests.settings.sqlite_sentinel_opts -x
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
### new
- added support for msgspec serialization (both json and msgpack)

### Breaking changes
- `BackendCommands` and `AsyncBackendCommands` are no longer decorated with `omit_exception`.
Expand Down
21 changes: 21 additions & 0 deletions django_valkey/serializers/msgspec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Any

import msgspec

from django_valkey.serializers.base import BaseSerializer


class MsgSpecJsonSerializer(BaseSerializer):
def dumps(self, value: Any) -> bytes:
return msgspec.json.encode(value)

def loads(self, value: bytes) -> Any:
return msgspec.json.decode(value)


class MsgSpecMsgPackSerializer(BaseSerializer):
def dumps(self, value: Any) -> bytes:
return msgspec.msgpack.encode(value)

def loads(self, value: bytes) -> Any:
return msgspec.msgpack.decode(value)
46 changes: 43 additions & 3 deletions docs/configure/advanced_configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,12 @@ and you're good to go

### Use Msgpack serializer

to use the msgpack serializer you should first install the msgpack package as explained in :ref:`msgpack`
to use the msgpack serializer you should first install the msgpack package

```shell
pip install django-valkey[msgpack]
```

then configure your settings like this:

```python
Expand All @@ -219,8 +224,43 @@ CACHES = {

and done

### Fun fact
you can serialize every type in the python built-ins, and probably non built-ins, but you have to check which serializer supports that type.
### Use msgspec serializers

msgspec comes with two serializers, one for json, one for msgpack

to use msgspec, first install the extra package:
```shell
pip install django-valkey[msgspec]
```

to use the json serializer, configure your backend like this:

```python
CACHES = {
"default": {
# ...
"OPTIONS": {
"SERIALIZER": "django_valkey.serializer.msgspec.MsgSpecJsonSerializer",
# ...
}
}
}
```

to use the msgpack serializer, configure it like this:

```python
CACHES = {
"default": {
# ...
"OPTIONS": {
"SERIALIZER": "django_valkey.serializer.msgspec.MsgSpecMsgPackSerializer",
# ...
}
}
}
```


## Pluggable Compressors

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ brotli = [
"brotli>=1.1.0",
]

msgspec = [
"msgspec>=0.19.0",
]

[dependency-groups]
dev = [
"anyio>=4.9.0",
Expand Down
41 changes: 41 additions & 0 deletions tests/settings/sqlite_msgspec_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
SECRET_KEY = "django_tests_secret_key"

CACHES = {
"default": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": ["valkey://127.0.0.1:6379?db=1", "valkey://127.0.0.1:6379?db=1"],
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecJsonSerializer",
},
},
"doesnotexist": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": "valkey://127.0.0.1:56379?db=1",
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecJsonSerializer",
},
},
"sample": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": "valkey://127.0.0.1:6379?db=1,valkey://127.0.0.1:6379?db=1",
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecJsonSerializer",
},
},
"with_prefix": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": "valkey://127.0.0.1:6379?db=1",
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecJsonSerializer",
},
"KEY_PREFIX": "test-prefix",
},
}

INSTALLED_APPS = ["django.contrib.sessions"]

USE_TZ = False
41 changes: 41 additions & 0 deletions tests/settings/sqlite_msgspec_msgpack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
SECRET_KEY = "django_tests_secret_key"

CACHES = {
"default": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": ["valkey://127.0.0.1:6379?db=1", "valkey://127.0.0.1:6379?db=1"],
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecMsgPackSerializer",
},
},
"doesnotexist": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": "valkey://127.0.0.1:56379?db=1",
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecMsgPackSerializer",
},
},
"sample": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": "valkey://127.0.0.1:6379?db=1,valkey://127.0.0.1:6379?db=1",
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecMsgPackSerializer",
},
},
"with_prefix": {
"BACKEND": "django_valkey.cache.ValkeyCache",
"LOCATION": "valkey://127.0.0.1:6379?db=1",
"OPTIONS": {
"CLIENT_CLASS": "django_valkey.client.DefaultClient",
"SERIALIZER": "django_valkey.serializers.msgspec.MsgSpecMsgPackSerializer",
},
"KEY_PREFIX": "test-prefix",
},
}

INSTALLED_APPS = ["django.contrib.sessions"]

USE_TZ = False
26 changes: 23 additions & 3 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from django_valkey.cluster_cache.client import DefaultClusterClient
from django_valkey.serializers.json import JSONSerializer
from django_valkey.serializers.msgpack import MSGPackSerializer
from django_valkey.serializers.msgspec import (
MsgSpecJsonSerializer,
MsgSpecMsgPackSerializer,
)
from django_valkey.serializers.pickle import PickleSerializer


Expand Down Expand Up @@ -128,7 +132,15 @@ def test_save_unicode(self, cache: ValkeyCache):
assert res == "heló"

def test_save_dict(self, cache: ValkeyCache):
if isinstance(cache.client._serializer, (JSONSerializer, MSGPackSerializer)):
if isinstance(
cache.client._serializer,
(
JSONSerializer,
MSGPackSerializer,
MsgSpecJsonSerializer,
MsgSpecMsgPackSerializer,
),
):
# JSONSerializer and MSGPackSerializer use the isoformat for
# datetimes.
now_dt: str | datetime.datetime = datetime.datetime.now().isoformat()
Expand Down Expand Up @@ -1099,9 +1111,17 @@ def test_sismember_memoryview(self, cache: ValkeyCache):
assert cache.sismember("foo", wrong_val) is False

def test_sismember_complex(self, cache: ValkeyCache):
if isinstance(cache.client._serializer, (JSONSerializer, MSGPackSerializer)):
if isinstance(
cache.client._serializer,
(
JSONSerializer,
MSGPackSerializer,
MsgSpecJsonSerializer,
MsgSpecMsgPackSerializer,
),
):
pytest.skip(
"JSONSerializer/MSGPackSerializer doesn't support the complex type"
"JSONSerializer/MSGPackSerializer and neither the msgspec serializers don't support the complex type"
)
cache.sadd("foo", 3j)
assert cache.sismember("foo", 3j) is True
Expand Down