Source code for attr_utils.serialise

#!/usr/bin/env python
#
#  annotations.py
"""
Add serialisation and deserialisation capability to attrs_ classes.

.. _attrs: https://www.attrs.org/en/stable/

Based on `attrs-serde <https://github.com/jondot/attrs-serde>`_.

Example usage
--------------

.. code-block:: python

	>>> import attr
	>>> from attr_utils.serialise import serde

	>>> person_dict = {"contact": {"personal": {"name": "John"}, "phone": "555-112233"}}

	>>> name_path = ["contact", "personal", "name"]
	>>> phone_path = ["contact", "phone"]

	>>> @serde
	... @attr.s
	... class Person(object):
	... 	name = attr.ib(metadata={"to": name_path, "from": name_path})
	... 	phone = attr.ib(metadata={"to": phone_path, "from": phone_path})

	>>> p = Person.from_dict(person_dict)
	Person(name=John phone=555-112233)

	>>> p.to_dict
	{"contact": {"personal": {"name": "John"}, "phone": "555-112233"}}


API Reference
---------------
"""

#  From https://github.com/jondot/attrs-serde
#  MIT License
#
#  Copyright (c) 2019 Dotan Nahum
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#  SOFTWARE.
#

# stdlib
from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, Optional, Type, Union, overload

try:
	# 3rd party
	from cytoolz import curried  # type: ignore
except ImportError:
	from toolz import curried  # type: ignore

# 3rd party
from attr import asdict, fields

__all__ = ["serde"]

if TYPE_CHECKING:
	AttrsClass = Any
else:
	# this package
	from attr_utils.utils import AttrsClass


@overload
def serde(
		cls: Type,
		from_key: str = ...,
		to_key: str = ...,
		) -> Type[AttrsClass]: ...


@overload
def serde(
		cls: None = None,
		from_key: str = ...,
		to_key: str = ...,
		) -> Callable[[Type[AttrsClass]], Type[AttrsClass]]: ...


[docs]def serde( cls: Optional[Type[AttrsClass]] = None, from_key: str = "from", to_key: str = "to", ) -> Union[Type[AttrsClass], Callable[[Type[AttrsClass]], Type[AttrsClass]]]: r""" Decorator to add serialisation and deserialisation capabilities to attrs classes. The keys used in the dictionary output, and used when creating the class from a dictionary, can be controlled using the ``metadata`` argument in :func:`attr.ib`: .. code-block:: from attr_utils.serialize import serde import attr @serde @attr.s class Person(object): name = attr.ib(metadata={"to": name_path, "from": name_path}) phone = attr.ib(metadata={"to": phone_path, "from": phone_path}) The names of the keys given in the ``metadata`` argument can be controlled with the ``from_key`` and ``to_key`` arguments: .. code-block:: from attr_utils.serialize import serde import attr @serde(from_key="get", to_key="set") @attr.s class Person(object): name = attr.ib(metadata={"get": name_path, "set": name_path}) phone = attr.ib(metadata={"get": phone_path, "set": phone_path}) This may be required when using other extensions to attrs. :param cls: The attrs class to add the methods to. :param from_key: :param to_key: :rtype: .. latex:vspace:: 20px Classes decorated with :deco:`~attr_utils.serialise.serde` will have two new methods added: .. py:classmethod:: from_dict(d) Construct an instance of the class from a dictionary. :param d: The dictionary. :type d: :class:`~typing.Mapping`\[:class:`str`, :py:obj:`~typing.Any`\] .. py:method:: to_dict(convert_values=False): Returns a dictionary containing the contents of the class. :param convert_values: Recurse into other attrs classes, and convert tuples, sets etc. into lists. This may be required to later construct a new class from the dictionary if the class uses complex converter functions. :type convert_values: :class:`bool` :rtype: :class:`~typing.MutableMapping`\[:class:`str`, :py:obj:`~typing.Any`\] .. versionchanged:: 0.5.0 By default values are left unchanged. In version 0.4.0 these were converted to basic Python types, which may be undesirable. The original behaviour can be restored using the ``convert_values`` parameter. """ def serde_with_class(cls: Type[AttrsClass]) -> Type[AttrsClass]: def from_dict(cls, d: Mapping[str, Any]): from_fields = list(map(lambda a: (a, curried.get_in([from_key], a.metadata, [a.name])), fields(cls))) return cls(**dict(map( lambda f: (f[0].name, curried.get_in(f[1], d, f[0].default)), from_fields, ))) def to_dict(self, convert_values: bool = False) -> MutableMapping[str, Any]: to_fields = curried.pipe( fields(self.__class__), curried.map(lambda a: (a, curried.get_in([to_key], a.metadata))), curried.filter(lambda f: f[1]), list, ) if convert_values: d = asdict(self) else: d = {a.name: getattr(self, a.name) for a in fields(self.__class__)} if not to_fields: return d return curried.reduce( lambda acc, f: curried.update_in(acc, f[1], lambda _: d[f[0].name]), to_fields, {}, ) from_dict.__doc__ = f""" Construct an instance of :class:`~.{cls.__name__}` from a dictionary. :param d: The dictionary. """ from_dict.__qualname__ = f"{cls.__name__}.from_dict" from_dict.__module__ = cls.__module__ cls.from_dict = classmethod(from_dict) to_dict.__doc__ = f""" Returns a dictionary containing the contents of the :class:`~.{cls.__name__}` object. :param convert_values: Recursively convert values into dictionaries, lists etc. as appropriate. """ to_dict.__qualname__ = f"{cls.__name__}.to_dict" to_dict.__module__ = cls.__module__ cls.to_dict = to_dict return cls if cls is not None: return serde_with_class(cls) else: return serde_with_class