Skip to content

Commit 595ea05

Browse files
tanbroclaude
andcommitted
feat(serializer): add dill serializer support
- Add dill to SerializerName type in typing.py - Implement dill serializer in cache.py with optional import - Add dill to optional dependencies in pyproject.toml - Add test_dill function in test_3rd_serializers.py - Update README.md to document dill serializer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8f38b18 commit 595ea05

5 files changed

Lines changed: 71 additions & 4 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ We can see that the second call to `a_slow_func()` is served from the cache, whi
7575
- Simple [decorator][] syntax supporting both **`async`** and common functions, **asynchronous** and synchronous I/O.
7676
- Support [Redis][] **cluster**.
7777
- Multiple caching policies: LRU, FIFO, LFU, RR ...
78-
- Serialization formats: JSON, Pickle, MsgPack, YAML, BSON, CBOR ...
78+
- Serialization formats: JSON, Pickle, Dill, MsgPack, YAML, BSON, CBOR, cloudpickle ...
7979

8080
## Installation
8181

@@ -462,9 +462,9 @@ def my_func_with_complex_return(x):
462462
return {...} # Complex object
463463
```
464464
465-
Supported serializers: JSON, Pickle, MsgPack, YAML, BSON, CBOR, and cloudpickle.
465+
Supported serializers: JSON, Pickle, Dill, MsgPack, YAML, BSON, CBOR, and cloudpickle.
466466
467-
> ⚠️ **Warning:** [`pickle`][] can execute arbitrary code during deserialization. Use with extreme caution, especially with untrusted data.
467+
> ⚠️ **Warning:** [`pickle`][] and `dill` can execute arbitrary code during deserialization. Use with extreme caution, especially with untrusted data.
468468
469469
### Handling Non-Serializable Arguments
470470
@@ -1057,6 +1057,7 @@ graph LR
10571057
LruTScriptsMixin --> lru_t_put.lua
10581058
Serializer --> json
10591059
Serializer --> pickle
1060+
Serializer --> dill
10601061
Serializer --> msgpack
10611062
Serializer --> bson
10621063
Serializer --> yaml

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ source = "https://github.com/tanbro/redis_func_cache.git"
4242
[project.optional-dependencies]
4343
hiredis = ["redis[hiredis]"]
4444
pygments = ["Pygments>=2.9"]
45+
dill = ["dill>=0.3.6"]
4546
bson = ["pymongo>=3.9"]
4647
msgpack = ["msgpack>=1.0"]
4748
yaml = ["PyYAML>=5.4"]
@@ -50,6 +51,7 @@ cloudpickle = ["cloudpickle>=3.0"]
5051
all = [
5152
"redis[hiredis]",
5253
"Pygments>=2.9",
54+
"dill>=0.3.6",
5355
"pymongo>=3.9",
5456
"msgpack>=1.0",
5557
"PyYAML>=5.4",

src/redis_func_cache/cache.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
from redis.commands.core import AsyncScript, Script
1919

20+
try: # pragma: no cover
21+
import dill # type: ignore[import-not-found]
22+
except ImportError: # pragma: no cover
23+
dill = None # type: ignore[assignment]
2024
try: # pragma: no cover
2125
import bson # type: ignore[import-not-found]
2226
except ImportError: # pragma: no cover
@@ -222,6 +226,7 @@ def __init__(
222226
223227
- ``"json"``: Use :func:`json.dumps` and :func:`json.loads`
224228
- ``"pickle"``: Use :func:`pickle.dumps` and :func:`pickle.loads`
229+
- ``"dill"``: Use :func:`dill.dumps` and :func:`dill.loads`. Only available when `dill <https://pypi.org/project/dill/>`_ is installed.
225230
- ``"bson"``: Use :func:`bson.decode` and :func:`bson.encode`. Only available when `pymongo <https://pypi.org/project/pymongo/>`_ is installed.
226231
- ``"msgpack"``: Use :func:`msgpack.packb` and :func:`msgpack.unpackb`. Only available when :mod:`msgpack` is installed.
227232
- ``"cbor"``: Use :func:`cbor2.dumps` and :func:`cbor2.loads`. Only available when `cbor2 <https://pypi.org/project/cbor2/>`_ is installed.
@@ -301,6 +306,11 @@ def my_deserializer(data):
301306
"json": (lambda x: json.dumps(x).encode(), lambda x: json.loads(x)),
302307
"pickle": (lambda x: pickle.dumps(x), lambda x: pickle.loads(x)),
303308
}
309+
if dill is not None: # pragma: no cover
310+
__serializers__["dill"] = (
311+
lambda x: dill.dumps(x), # pyright: ignore[reportOptionalMemberAccess]
312+
lambda x: dill.loads(x), # pyright: ignore[reportOptionalMemberAccess]
313+
)
304314
if bson is not None: # pragma: no cover
305315
__serializers__["bson"] = (
306316
lambda x: bson.encode({"": x}), # pyright: ignore[reportOptionalMemberAccess]

src/redis_func_cache/typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
RedisScriptT = Union[redis.commands.core.Script, redis.commands.core.AsyncScript]
4747

4848

49-
SerializerName = Literal["json", "pickle", "bson", "msgpack", "yaml", "cbor", "cloudpickle"]
49+
SerializerName = Literal["json", "pickle", "dill", "bson", "msgpack", "yaml", "cbor", "cloudpickle"]
5050

5151

5252
if TYPE_CHECKING: # pragma: no cover

tests/test_3rd_serializers.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,57 @@ def _test_datetime(f):
230230
_test_int(fn)
231231
_test_float(fn)
232232
_test_datetime(fn)
233+
234+
235+
def test_dill():
236+
def _test_bytes(f):
237+
for _ in range(randint(1, MAXSIZE * 2)):
238+
v0 = uuid4().bytes
239+
v1 = f(v0)
240+
assert v0 == v1
241+
242+
def _test_str(f):
243+
for _ in range(randint(1, MAXSIZE * 2)):
244+
v0 = uuid4().hex
245+
v1 = f(v0)
246+
assert v0 == v1
247+
248+
def _test_int(f):
249+
for _ in range(randint(1, MAXSIZE * 2)):
250+
v0 = randint(-(2**63), 2**63 - 1)
251+
v1 = f(v0)
252+
assert v0 == v1
253+
254+
def _test_float(f):
255+
for _ in range(randint(1, MAXSIZE * 2)):
256+
v0 = random()
257+
v1 = f(v0)
258+
assert v0 == v1
259+
260+
def _test_bool(f):
261+
for _ in range(randint(1, MAXSIZE * 2)):
262+
v0 = choice((True, False))
263+
v1 = f(v0)
264+
assert v0 == v1
265+
266+
def _test_none(f):
267+
for _ in range(randint(1, MAXSIZE * 2)):
268+
v0 = None
269+
v1 = f(v0)
270+
assert v0 == v1
271+
272+
def _test_datetime(f):
273+
for _ in range(randint(1, MAXSIZE * 2)):
274+
v0 = datetime.now().replace(microsecond=0)
275+
v1 = f(v0)
276+
assert v0 == v1
277+
278+
for cache in CACHES.values():
279+
fn = cache(echo, serializer="dill")
280+
_test_bytes(fn)
281+
_test_none(fn)
282+
_test_bool(fn)
283+
_test_str(fn)
284+
_test_int(fn)
285+
_test_float(fn)
286+
_test_datetime(fn)

0 commit comments

Comments
 (0)