Source code for jsoner.serialization

# -*- coding: utf-8 -*-

import abc
import json
import typing as T
from functools import partial
from inspect import signature

from .registry import decoders
from .registry import encoders
from .registry import import_object


[docs]class DictConvertible(abc.ABC): """ This abstract class implements the :meth:`to_dict` and :meth:`from_dict`. Every class implementing those two methods will be a subclass of :class:`DictConvertible`. It is not necessary to inherit from this class. """ @abc.abstractmethod def to_dict(self): raise NotImplementedError @classmethod @abc.abstractmethod def from_dict(cls, data_dict: dict): raise NotImplementedError @classmethod def __subclasshook__(cls, other_cls: type): is_dict_convertible = (hasattr(other_cls, 'to_dict') and hasattr(other_cls, 'from_dict')) return is_dict_convertible
[docs]class StrConvertible(abc.ABC): """ This abstract class implements the :meth:`to_str` and :meth:`from_str`. Every class implementing those two methods will be a subclass of :class:`StrConvertible`. It is not necessary to inherit from this class. """ @abc.abstractmethod def to_str(self): raise NotImplementedError @classmethod @abc.abstractmethod def from_str(cls, json_str: str): raise NotImplementedError @classmethod def __subclasshook__(cls, other_cls: type): is_str_convertible = (hasattr(other_cls, 'to_str') and hasattr(other_cls, 'from_str')) return is_str_convertible
[docs]class JsonerSerializable(abc.ABC): """ The :class:`JsonerSerializable` serves as an abstract class which indicated if an instance can be serialized by *Jsoner*. Therefore it implements the :meth:`__subclasshook__` method. An object is serializable by *Jsoner* if it is registered in the encoding-, decoding-registry or if it is convertible to a dict or to a string. """ @classmethod def __subclasshook__(cls, other_cls: type): try: is_serializable = other_cls in decoders and other_cls in encoders except TypeError: return False if not is_serializable: is_serializable |= (issubclass(other_cls, StrConvertible) or issubclass(other_cls, DictConvertible)) return is_serializable
def _is_instance_of_type(obj_or_type: T.Union[object, type]) -> bool: """ If the argument of this function is an instance of :class:`type`, the function returns true. .. note:: If the argument is a class which is defined in a local namespace, the function return false as well. :param obj_or_type: :return: """ is_a_cls = isinstance(obj_or_type, type) is_a_cls &= '<locals>' not in obj_spec(obj_or_type) return is_a_cls
[docs]def obj_spec(obj_or_type: T.Union[object, type]) -> str: """ This function returns the path of the argument class. If the argument is an instance of :class:`type`, it returns the path of the argument itself. Usage:: >>> from jsoner.serialization import obj_spec >>> class A: ... pass >>> obj_spec(A) # doctest: +ELLIPSIS '...A' >>> a = A() >>> obj_spec(a) # doctest: +ELLIPSIS '...A' :param obj_or_type: :return: """ if isinstance(obj_or_type, type): path = obj_or_type.__module__ + '.' + obj_or_type.__qualname__ else: path = obj_or_type.__class__.__module__ + '.' + obj_or_type.__class__.__qualname__ return path
[docs]class JsonEncoder(json.JSONEncoder): """ JsonEncoder will decode all objects, which implement either `to_dict` and `from_dict` or `to_str` and `from_str`. .. note:: :meth:`from_str` and :meth:`from_dict` must be a ``classmethod``. It is enough to implement either :meth:`from_str` and :meth:`to_str` or :meth:`from_dict` and :meth:`to_dict`. If both are implemented, then :meth:`from_dict` and :meth:`to_dict` are preferred. If you do not want to implement methods in your class, or you might have no access to the class definition, you can use :func:`jsoner.registry.encoders` and :func:`jsoner.registry.decoders`. """
[docs] def default(self, obj, *args, **kwargs): if isinstance(obj, JsonerSerializable): obj_dict = { '__obj_cls__': obj_spec(obj), '__json_data__': None } if isinstance(obj, DictConvertible): obj_data = obj.to_dict() elif isinstance(obj, StrConvertible): obj_data = obj.to_str() else: encoder = encoders.get(obj) if isinstance(encoder, T.Callable): obj_data = encoder(obj) else: obj_data = encoder obj_dict['__json_data__'] = obj_data return obj_dict elif _is_instance_of_type(obj): return {'__cls__': obj_spec(obj)} else: return super().default(obj)
[docs]def json_hook(primitive: T.Any) -> T.Any: """ This hook will try to recreate an object from the data it receives. It it fails to do so, it will just return the original data. :param primitive: :return: """ if not isinstance(primitive, T.Dict): return primitive else: return maybe_convert_to_obj(primitive)
[docs]def maybe_convert_to_obj(data: dict) -> T.Any: """ This function will try to create an object from the data dictionary. :param data: :return: """ if '__cls__' in data: _cls = data.get('__cls__', '') try: return import_object(_cls) except ImportError: return data elif '__obj_cls__' in data: _cls = data.get('__obj_cls__', '') try: cls = import_object(_cls) except ImportError: return data obj_data = data.get('__json_data__') if issubclass(cls, DictConvertible): return cls.from_dict(obj_data) elif issubclass(cls, StrConvertible): return cls.from_str(obj_data) else: decoder = decoders.get(cls) if decoder is None: return data if callable(decoder): sig = signature(decoder) if len(sig.parameters) == 1: return decoder(obj_data) else: return decoder(obj_data, cls) else: return decoder or data else: return data
dump = partial(json.dump, cls=JsonEncoder) dumps = partial(json.dumps, cls=JsonEncoder) load = partial(json.load, object_hook=json_hook) loads = partial(json.loads, object_hook=json_hook)