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 plain, Localizable, _
from betty.serde.dump import Dump, VoidableDump
if TYPE_CHECKING:
from betty.locale 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 plain("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 plain("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