# -*- 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 . # ########################################################################## 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