Source code for prov.serializers
from __future__ import annotations # needed for | type annotations in Python < 3.10
import io
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, ClassVar
from prov import Error
if TYPE_CHECKING:
from prov.model import ProvDocument
__author__ = "Trung Dong Huynh"
__email__ = "trungdong@donggiang.com"
__all__ = ["Registry", "Serializer", "get"]
def _is_text_stream(stream: io.IOBase) -> bool:
"""Return whether ``stream`` accepts/produces ``str`` rather than bytes.
``isinstance(stream, io.TextIOBase)`` alone misses file-like wrappers
such as ``tempfile.NamedTemporaryFile``'s ``_TemporaryFileWrapper``,
which proxy a text stream without subclassing ``io.TextIOBase``. Such
wrappers expose the underlying text stream's ``encoding`` attribute,
while binary streams (``io.BytesIO``, buffered readers/writers) have no
``encoding``, so the extra check only ever reclassifies text-stream
proxies that would otherwise crash on bytes (#240).
"""
return isinstance(stream, io.TextIOBase) or hasattr(stream, "encoding")
[docs]
class Serializer(ABC):
"""Serializer for PROV documents."""
document = None
"""PROV document to serialise."""
def __init__(self, document: ProvDocument | None = None):
"""Create a serializer bound to a document.
Args:
document: Document this serializer will serialize, or
deserialize into. May be ``None`` when only deserialization
(which produces its own new document) is needed.
"""
self.document = document
[docs]
@abstractmethod
def serialize(self, stream: io.IOBase, **args: Any) -> None:
"""Serialize ``self.document`` and write it to a stream.
Subclasses implement this to encode the bound document and write the
result to ``stream``.
Args:
stream: Stream to write the serialized document into.
**args: Format-specific serialization options, passed through by
subclasses.
"""
pass # pragma: no cover -- abstract body, never executed directly
[docs]
@abstractmethod
def deserialize(self, stream: io.IOBase, **args: Any) -> ProvDocument:
"""Read and parse a document from a stream.
Subclasses implement this to parse ``stream`` and build a new
:class:`~prov.model.ProvDocument` from it.
Args:
stream: Stream to deserialize the document from.
**args: Format-specific deserialization options, passed through
by subclasses.
Returns:
The deserialized :class:`~prov.model.ProvDocument`.
"""
pass # pragma: no cover -- abstract body, never executed directly
[docs]
class DoNotExist(Error):
"""Exception for the case a serializer is not available."""
pass
[docs]
class Registry:
"""Registry of serializers."""
serializers: ClassVar[dict[str, type[Serializer]] | None] = None
"""Property caching all available serializers in a dict."""
[docs]
@staticmethod
def load_serializers() -> None:
"""Load all serializers whose optional dependencies are installed.
``json`` and ``provn`` have no optional dependencies and are always
registered. ``rdf`` (needs ``rdflib``) and ``xml`` (needs ``lxml``)
are registered only if their extra is installed, so that ``import
prov`` and JSON/PROV-N work in a minimal install; requesting an
unavailable format then raises an informative :class:`DoNotExist`
(see :func:`get`) rather than a bare ``ModuleNotFoundError``.
The insertion order is kept as ``json, rdf, provn, xml`` (the
historic order when all extras are present) because
:func:`prov.read`'s format auto-detection iterates
``Registry.serializers`` in order and several tests
(``test_read_auto_detects_rdf``,
``test_read_auto_detect_of_xml_hits_uncaught_rdf_syntax_error``,
``test_read_on_unparseable_content_raises_bad_syntax``) pin the
exact candidate tried second.
"""
from prov.serializers.provjson import ProvJSONSerializer
from prov.serializers.provn import ProvNSerializer
serializers: dict[str, type[Serializer]] = {
"json": ProvJSONSerializer,
}
try:
from prov.serializers.provrdf import ProvRDFSerializer
except ImportError: # pragma: no cover -- rdflib (rdf extra) absent; covered by the minimal-install CI job
pass
else:
serializers["rdf"] = ProvRDFSerializer
serializers["provn"] = ProvNSerializer
try:
from prov.serializers.provxml import ProvXMLSerializer
except ImportError: # pragma: no cover -- lxml (xml extra) absent; covered by the minimal-install CI job
pass
else:
serializers["xml"] = ProvXMLSerializer
Registry.serializers = serializers
#: Formats provided by optional extras, used to build the DoNotExist message
#: in :func:`get` when the corresponding dependency is not installed.
_OPTIONAL_FORMAT_EXTRAS = {"rdf": "rdf", "xml": "xml"}
[docs]
def get(format_name: str) -> type[Serializer]:
"""Return the serializer class registered for a format.
Args:
format_name: Registry key, e.g. ``"json"``, ``"xml"``, ``"rdf"``,
``"provn"``.
Returns:
The :class:`Serializer` subclass for the format.
Raises:
DoNotExist: If no serializer is registered under ``format_name``.
"""
# Lazily initialize the list of serializers to avoid cyclic imports
if Registry.serializers is None:
Registry.load_serializers()
serializers = Registry.serializers
assert serializers is not None # load_serializers() always populates it
try:
return serializers[format_name]
except KeyError as e:
extra = _OPTIONAL_FORMAT_EXTRAS.get(format_name)
# The informative-message branch is reachable only when the optional
# extra is absent; the minimal-install CI job exercises it.
if extra is not None: # pragma: no cover
raise DoNotExist(
f'Serializer for the "{format_name}" format requires the '
f'"{extra}" extra; install it with: pip install "prov[{extra}]"'
) from e
raise DoNotExist(
f'No serializer available for the format "{format_name}"'
) from e