From 1010e04a7c0e55df1a07d0a09b40247b6e2af590 Mon Sep 17 00:00:00 2001 From: kostahuffman Date: Fri, 14 Mar 2025 13:21:52 +1100 Subject: [PATCH 1/4] Add yaml serializer --- aiocache/serializers/serializers.py | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/aiocache/serializers/serializers.py b/aiocache/serializers/serializers.py index 58a5b61b..c8d0ebdd 100644 --- a/aiocache/serializers/serializers.py +++ b/aiocache/serializers/serializers.py @@ -1,5 +1,6 @@ import logging import pickle # noqa: S403 +import yaml from abc import ABC, abstractmethod from typing import Any, Optional @@ -197,3 +198,38 @@ def loads(self, value): if value is None: return None return msgpack.loads(value, raw=raw, use_list=self.use_list) + + +class YamlSerializer(BaseSerializer): + """ + Transform data to YAML string with ``yaml.dump`` and ``yaml.load`` to retrieve it back. + """ + + def __init__(self, *args, loader=yaml.SafeLoader, **kwargs): + """ + Initialize the YamlSerializer with the specified loader. + + :param loader: The YAML loader to use for deserialization. Default is yaml.SafeLoader. + """ + super().__init__(*args, **kwargs) + self.loader = loader + + def dumps(self, value): + """ + Serialize the received value using ``yaml.dump``. + + :param value: obj + :returns: str + """ + return yaml.dump(value) + + def loads(self, value): + """ + Deserialize value using ``yaml.load``. + + :param value: str + :returns: obj + """ + if value is None: + return None + return yaml.load(value, Loader=self.loader) From c21aad1c4b4f6c09125c24e51d862a3fee6ccb7f Mon Sep 17 00:00:00 2001 From: kostahuffman Date: Fri, 14 Mar 2025 13:27:08 +1100 Subject: [PATCH 2/4] docs: add yaml serializer to serializers.rst --- README.rst | 2 +- docs/serializers.rst | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9b7c65e5..f390abaa 100644 --- a/README.rst +++ b/README.rst @@ -166,7 +166,7 @@ How does it work Aiocache provides 3 main entities: - **backends**: Allow you specify which backend you want to use for your cache. Currently supporting: SimpleMemoryCache, RedisCache using redis_ and MemCache using aiomcache_. -- **serializers**: Serialize and deserialize the data between your code and the backends. This allows you to save any Python object into your cache. Currently supporting: StringSerializer, PickleSerializer, JsonSerializer, and MsgPackSerializer. But you can also build custom ones. +- **serializers**: Serialize and deserialize the data between your code and the backends. This allows you to save any Python object into your cache. Currently supporting: StringSerializer, PickleSerializer, JsonSerializer, MsgPackSerializer, and YamlSerializer. But you can also build custom ones. - **plugins**: Implement a hooks system that allows to execute extra behavior before and after of each command. If you are missing an implementation of backend, serializer or plugin you think it could be interesting for the package, do not hesitate to open a new issue. diff --git a/docs/serializers.rst b/docs/serializers.rst index b68b20f6..bd8ee04b 100644 --- a/docs/serializers.rst +++ b/docs/serializers.rst @@ -54,6 +54,14 @@ MsgPackSerializer .. autoclass:: aiocache.serializers.MsgPackSerializer :members: +.. _yamlserializer: + +YamlSerializer +-------------- + +.. autoclass:: aiocache.serializers.YamlSerializer + :members: + In case the current serializers are not covering your needs, you can always define your custom serializer as shown in ``examples/serializer_class.py``: .. literalinclude:: ../examples/serializer_class.py @@ -66,4 +74,4 @@ You can also use marshmallow as your serializer (``examples/marshmallow_serializ :language: python :linenos: -By default cache backends assume they are working with ``str`` types. If your custom implementation transform data to bytes, you will need to set the class attribute ``encoding`` to ``None``. +By default cache backends assume they are working with ``str`` types. If your custom implementation transform data to bytes, you will need to set the class attribute ``encoding`` to ``None``. \ No newline at end of file From a1ade11a4a9fff897dd062abcb84108779bfecda Mon Sep 17 00:00:00 2001 From: kostahuffman Date: Fri, 14 Mar 2025 13:29:58 +1100 Subject: [PATCH 3/4] tests: add YamlSerializer test --- aiocache/serializers/__init__.py | 2 ++ tests/ut/test_serializers.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/aiocache/serializers/__init__.py b/aiocache/serializers/__init__.py index c7499335..a6c07923 100644 --- a/aiocache/serializers/__init__.py +++ b/aiocache/serializers/__init__.py @@ -6,6 +6,7 @@ NullSerializer, PickleSerializer, StringSerializer, + YamlSerializer, ) logger = logging.getLogger(__name__) @@ -28,4 +29,5 @@ "PickleSerializer", "JsonSerializer", "MsgPackSerializer", + "YamlSerializer", ] diff --git a/tests/ut/test_serializers.py b/tests/ut/test_serializers.py index 33835531..f57bbc7e 100644 --- a/tests/ut/test_serializers.py +++ b/tests/ut/test_serializers.py @@ -1,4 +1,5 @@ import pickle +import yaml from collections import namedtuple from unittest import mock @@ -11,6 +12,7 @@ NullSerializer, PickleSerializer, StringSerializer, + YamlSerializer, ) @@ -173,3 +175,42 @@ def test_dumps_and_loads_dict(self): "a": [1, 2, ["1", 2]], "b": {"b": 1, "c": [1, 2]}, } + + +class TestYamlSerializer: + def test_init(self): + serializer = YamlSerializer() + assert isinstance(serializer, BaseSerializer) + assert serializer.DEFAULT_ENCODING == "utf-8" + assert serializer.encoding == "utf-8" + assert serializer.loader == yaml.SafeLoader + + def test_init_with_custom_loader(self): + serializer = YamlSerializer(loader=yaml.FullLoader) + assert serializer.loader == yaml.FullLoader + + @pytest.mark.parametrize("obj", TYPES) + def test_set_types(self, obj): + serializer = YamlSerializer() + assert serializer.loads(serializer.dumps(obj)) == obj + + def test_dumps(self): + serializer = YamlSerializer() + assert serializer.dumps({"hi": 1}) == "hi: 1\n" + + def test_dumps_with_none(self): + serializer = YamlSerializer() + assert serializer.dumps(None) == "null\n...\n" + + def test_loads(self): + serializer = YamlSerializer() + assert serializer.loads("hi: 1\n") == {"hi": 1} + + def test_loads_with_none(self): + serializer = YamlSerializer() + assert serializer.loads(None) is None + + def test_dumps_and_loads(self): + obj = {"hi": 1} + serializer = YamlSerializer() + assert serializer.loads(serializer.dumps(obj)) == obj From b2374995c7670b0130f3d3fe690a416c28d8256f Mon Sep 17 00:00:00 2001 From: kostahuffman Date: Fri, 14 Mar 2025 15:11:51 +1100 Subject: [PATCH 4/4] lint: fix --- aiocache/serializers/serializers.py | 22 ++++++++++++---------- requirements.txt | 1 + tests/ut/test_serializers.py | 13 +++++++------ 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/aiocache/serializers/serializers.py b/aiocache/serializers/serializers.py index c8d0ebdd..7a61b975 100644 --- a/aiocache/serializers/serializers.py +++ b/aiocache/serializers/serializers.py @@ -1,6 +1,5 @@ import logging import pickle # noqa: S403 -import yaml from abc import ABC, abstractmethod from typing import Any, Optional @@ -18,6 +17,12 @@ msgpack = None logger.debug("msgpack not installed, MsgPackSerializer unavailable") +try: + import yaml # noqa: I900 +except ImportError: + yaml = None + logger.debug("yaml not installed, YamlSerializer unavailable") + _NOT_SET = object() @@ -202,17 +207,14 @@ def loads(self, value): class YamlSerializer(BaseSerializer): """ - Transform data to YAML string with ``yaml.dump`` and ``yaml.load`` to retrieve it back. + Transform data to YAML string with ``yaml.dump`` and ``yaml.load`` to retrieve it back. You need + to have ``yaml`` installed in order to be able to use this serializer. """ - def __init__(self, *args, loader=yaml.SafeLoader, **kwargs): - """ - Initialize the YamlSerializer with the specified loader. - - :param loader: The YAML loader to use for deserialization. Default is yaml.SafeLoader. - """ + def __init__(self, *args, **kwargs): + if not yaml: + raise RuntimeError("yaml not installed, YamlSerializer unavailable") super().__init__(*args, **kwargs) - self.loader = loader def dumps(self, value): """ @@ -232,4 +234,4 @@ def loads(self, value): """ if value is None: return None - return yaml.load(value, Loader=self.loader) + return yaml.safe_load(value) diff --git a/requirements.txt b/requirements.txt index 983c1694..f1992090 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ pytest-asyncio==0.25.3 pytest-cov==6.0.0 pytest-mock==3.14.0 redis==5.2.1 +pyyaml==6.0.2 \ No newline at end of file diff --git a/tests/ut/test_serializers.py b/tests/ut/test_serializers.py index f57bbc7e..77d7ffa8 100644 --- a/tests/ut/test_serializers.py +++ b/tests/ut/test_serializers.py @@ -1,5 +1,4 @@ import pickle -import yaml from collections import namedtuple from unittest import mock @@ -20,6 +19,7 @@ TYPES = [1, 2.0, "hi", True, ["1", 1], {"key": "value"}, Dummy(1, 2)] JSON_TYPES = [1, 2.0, "hi", True, ["1", 1], {"key": "value"}] +YAML_TYPES = [1, 2.0, "hi", True, ["1", 1], {"key": "value"}] class TestNullSerializer: @@ -183,13 +183,14 @@ def test_init(self): assert isinstance(serializer, BaseSerializer) assert serializer.DEFAULT_ENCODING == "utf-8" assert serializer.encoding == "utf-8" - assert serializer.loader == yaml.SafeLoader - def test_init_with_custom_loader(self): - serializer = YamlSerializer(loader=yaml.FullLoader) - assert serializer.loader == yaml.FullLoader + def test_init_fails_if_yaml_not_installed(self): + with mock.patch("aiocache.serializers.serializers.yaml", None): + with pytest.raises(RuntimeError): + YamlSerializer() + assert JsonSerializer(), "Other serializers should still initialize" - @pytest.mark.parametrize("obj", TYPES) + @pytest.mark.parametrize("obj", YAML_TYPES) def test_set_types(self, obj): serializer = YamlSerializer() assert serializer.loads(serializer.dumps(obj)) == obj