Source code for betty.serde.format

"""
Provide serialization formats.
"""

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from typing import cast, Sequence, TYPE_CHECKING, final

import yaml
from typing_extensions import override

from betty.assertion.error import AssertionFailed
from betty.locale.localizable import static, Localizable, _
from betty.serde.dump import Dump, VoidableDump

if TYPE_CHECKING:
    from betty.locale.localizer import Localizer


[docs] class Format(ABC): """ Defines a (de)serialization format. """ @property @abstractmethod def extensions(self) -> set[str]: """ The file extensions this format can (de)serialize. Extensions MUST NOT include a leading dot. """ pass @property @abstractmethod def label(self) -> Localizable: """ The format's human-readable label. """ pass
[docs] @abstractmethod def load(self, dump: str) -> Dump: """ Deserialize data. """ pass
[docs] @abstractmethod def dump(self, dump: VoidableDump) -> str: """ Serialize data. """ pass
[docs] @final class Json(Format): """ Defines the `JSON <https://json.org/>`_ (de)serialization format. """ @override @property def extensions(self) -> set[str]: return {"json"} @override @property def label(self) -> Localizable: return static("JSON")
[docs] @override def load(self, dump: str) -> Dump: try: return cast(Dump, json.loads(dump)) except json.JSONDecodeError as e: raise FormatError( _("Invalid JSON: {error}.").format(error=str(e)) ) from None
[docs] @override def dump(self, dump: VoidableDump) -> str: return json.dumps(dump)
[docs] @final class Yaml(Format): """ Defines the `YAML <https://yaml.org/>`_ (de)serialization format. """ @override @property def extensions(self) -> set[str]: return {"yaml", "yml"} @override @property def label(self) -> Localizable: return static("YAML")
[docs] @override def load(self, dump: str) -> Dump: try: return cast(Dump, yaml.safe_load(dump)) except yaml.YAMLError as e: raise FormatError( _("Invalid YAML: {error}.").format(error=str(e)) ) from None
[docs] @override def dump(self, dump: VoidableDump) -> str: return yaml.safe_dump(dump)
[docs] @final class FormatRepository: """ Exposes the available (de)serialization formats. """
[docs] def __init__( self, ): super().__init__() self._serde_formats = ( Json(), Yaml(), )
@property def formats(self) -> tuple[Format, ...]: """ All formats in this repository. """ return self._serde_formats @property def extensions(self) -> tuple[str, ...]: """ All file extensions supported by the formats in this repository. """ return tuple( extension for _format in self._serde_formats for extension in _format.extensions )
[docs] def format_for(self, extension: str) -> Format: """ Get the (de)serialization format for the given file extension. The extension MUST NOT include a leading dot. """ for serde_format in self._serde_formats: if extension in serde_format.extensions: return serde_format raise FormatError( _( 'Unknown file format ".{extension}". Supported formats are: {supported_formats}.' ).format(extension=extension, supported_formats=FormatStr(self.formats)) )
[docs] @final class FormatStr(Localizable): """ Localize and format a sequence of (de)serialization formats. """
[docs] def __init__(self, serde_formats: Sequence[Format]): self._serde_formats = serde_formats
[docs] @override def localize(self, localizer: Localizer) -> str: return ", ".join( [ f".{extension} ({serde_format.label.localize(localizer)})" for serde_format in self._serde_formats for extension in serde_format.extensions ] )
[docs] class FormatError(AssertionFailed): """ Raised when data that is being deserialized is provided in an unknown (undeserializable) format. """ pass # pragma: no cover