openlp/openlp/core/common/json.py

207 lines
8.3 KiB
Python

# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2022 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
from contextlib import suppress
from json import JSONDecoder, JSONEncoder
from pathlib import Path
_registered_classes = {}
class JSONMixin(object):
"""
:class:`JSONMixin` is a mixin class to simplify the serialization of a subclass to JSON.
:cvar:`_json_keys` is used to specify the attributes of the subclass that you wish to serialize.
:vartype _json_keys: list[str]
:cvar:`_name` set to override the subclass name, useful if using a `proxy` class
:vartype _name: str
"""
_json_keys = []
_name = None
_version = 1
def __init_subclass__(cls, register_names=None, **kwargs):
"""
Register the subclass.
:param collections.Iterable[str] register_names: Alternative names to register instead of the class name
:param kwargs: Other args to pass to the super method
:return None:
"""
super().__init_subclass__(**kwargs)
for key in register_names or [cls.__name__]:
_registered_classes[key] = cls
@classmethod
def encode_json(cls, obj, **kwargs):
"""
Create a instance of the subclass from the dictionary that has been constructed by the JSON representation.
Only use the keys specified in :cvar:`_json_keys`.
:param dict[str] obj: The dictionary representation of the subclass (deserailized from the JSON)
:param kwargs: Contains any extra parameters. Not used!
:return: The desrialized object
"""
return cls(**{key: obj[key] for key in cls._json_keys if obj.get(key) is not None})
@classmethod
def attach_meta(cls, j_dict):
"""
Attach meta data to the serialized dictionary.
:param dict[str] j_dict: The dictionary to update with the meta data
:return None:
"""
j_dict.update({'json_meta': {'class': cls._name or cls.__name__, 'version': cls._version}})
def json_object(self, **kwargs):
"""
Create a dictionary that can be JSON decoded.
:param kwargs: Contains any extra parameters. Not used!
:return dict[str]: The dictionary representation of this Path object.
"""
j_dict = {key: self.__dict__[key] for key in self._json_keys if self.__dict__.get(key) is not None}
self.attach_meta(j_dict)
return j_dict
class OpenLPJSONDecoder(JSONDecoder):
"""
Implement a custom JSONDecoder to extend compatibility to custom objects
Example Usage:
object = json.loads(json_string, cls=OpenLPJsonDecoder)
"""
def __init__(self, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True,
object_pairs_hook=None, **kwargs):
"""
Re-implement __init__ so that we can pass in our object_hook method. Any additional kwargs, are stored in the
instance and are passed to custom objects upon encoding or decoding.
"""
self.kwargs = kwargs
if object_hook is None:
object_hook = self.custom_object_hook
super().__init__(object_hook=object_hook, parse_float=parse_float, parse_int=parse_int,
parse_constant=parse_constant, strict=strict, object_pairs_hook=object_pairs_hook)
def custom_object_hook(self, obj):
"""
Implement a custom object decoder.
:param dict obj: A decoded JSON object
:return: The custom object from the serialized data if the custom object is registered, else obj
"""
if '__Path__' in obj:
return PathSerializer.encode_json(obj, **self.kwargs)
try:
key = obj['json_meta']['class']
except KeyError:
return obj
if key in _registered_classes:
return _registered_classes[key].encode_json(obj, **self.kwargs)
return obj
class OpenLPJSONEncoder(JSONEncoder):
"""
Implement a custom JSONEncoder to handle to extend compatibility to custom objects
Example Usage:
json_string = json.dumps(object, cls=OpenLPJSONEncoder)
"""
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
indent=None, separators=None, default=None, **kwargs):
"""
Re-implement __init__ so that we can pass in additional kwargs, which are stored in the instance and are passed
to custom objects upon encoding or decoding.
"""
self.kwargs = kwargs
if default is None:
default = self.custom_default
super().__init__(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, separators=separators,
default=default)
def custom_default(self, obj):
"""
Convert any registered objects into a dictionary object which can be serialized.
:param object obj: The object to convert
:return dict: The serializable object
"""
if isinstance(obj, JSONMixin):
return obj.json_object()
elif obj.__class__.__name__ in _registered_classes:
return _registered_classes[obj.__class__.__name__].json_object(obj, **self.kwargs)
return super().default(obj, **self.kwargs)
def is_serializable(obj):
return obj.__class__.__name__ in _registered_classes
class PathSerializer(JSONMixin, register_names=('Path', 'PosixPath', 'WindowsPath')):
"""
Implement a de/serializer for pathlib.Path objects
"""
_name = 'Path'
@staticmethod
def encode_json(obj, base_path=None, **kwargs):
"""
Reimplement encode_json to create a Path object from a dictionary representation.
:param dict[str] obj: The dictionary representation
:param Path base_path: If specified, an absolute path to base the relative path off of.
:param kwargs: Contains any extra parameters. Not used!
:return Path: The deserialized Path object
"""
if '__Path__' in obj:
parts = obj['__Path__']
else:
parts = obj['parts']
path = Path(*parts)
if base_path and not path.is_absolute():
return base_path / path
return path
@classmethod
def json_object(cls, obj, base_path=None, is_js=False, **kwargs):
"""
Create a dictionary that can be JSON decoded.
:param Path base_path: If specified, an absolute path to make a relative path from.
:param bool is_js: Encode the path as a uri. For example for use in the js rendering code.
:param kwargs: Contains any extra parameters. Not used!
:return: The dictionary representation of this Path object.
:rtype: dict[tuple]
"""
path = obj
if base_path:
with suppress(ValueError):
path = path.relative_to(base_path)
if is_js is True:
return path.as_uri()
json_dict = {'parts': path.parts}
cls.attach_meta(json_dict)
return json_dict