Source code for pyphi.serialize

"""Typed, compact (de)serialization of PyPhi results via msgspec.

Supports two wire formats from one schema: ``"json"`` (default, readable
structure) and ``"msgpack"`` (binary, compact). The document carries a single
top-level ``format_version`` and a node-labels frame written once per
document: nested objects whose labels match the frame store none and inherit
it on decode. A path ending in ``.gz`` is transparently gzip-compressed on
save and decompressed on load.
"""

import gzip
import os
from pathlib import Path
from typing import Any

import msgspec

from . import convert
from . import schema

FORMAT_VERSION = 2


class _Document(msgspec.Struct, frozen=True):
    format_version: int
    # A φ value serialized on its own is a native float; every other domain
    # object serializes to a tagged Struct in ``schema.Schema``.
    payload: schema.Schema | float
    # The document's node-labels frame: claimed once by the first labeled
    # payload object, inherited on decode by every object that carries none.
    node_labels: schema.NodeLabelsSchema | None = None


def _encoder(fmt: str):
    if fmt == "json":
        return msgspec.json.encode
    if fmt == "msgpack":
        return msgspec.msgpack.encode
    raise ValueError(f"Unknown format: {fmt!r}")


def _decode(data: bytes, fmt: str) -> _Document:
    if fmt == "json":
        return msgspec.json.decode(data, type=_Document)
    if fmt == "msgpack":
        return msgspec.msgpack.decode(data, type=_Document)
    raise ValueError(f"Unknown format: {fmt!r}")


[docs] def dumps(obj: Any, *, format: str = "json") -> bytes: payload, frame = convert.encode_document(obj) doc = _Document(format_version=FORMAT_VERSION, payload=payload, node_labels=frame) return _encoder(format)(doc)
[docs] def loads(data: bytes, *, format: str = "json", node_labels: Any = None) -> Any: """Deserialize a document produced by :func:`dumps`. Parameters ---------- data : bytes The serialized document. format : {"json", "msgpack"}, optional Wire format. Defaults to ``"json"``. node_labels : NodeLabels, optional Replacement label frame. If given, it is used in place of the document's stored frame; objects carrying their own per-object labels keep them. """ doc = _decode(data, format) if doc.format_version > FORMAT_VERSION: raise ValueError( f"cannot load format_version {doc.format_version}: this version of " f"PyPhi reads format_version {FORMAT_VERSION} or lower" ) return convert.decode_document(doc.payload, doc.node_labels, node_labels=node_labels)
_SUFFIX_FORMATS = {".json": "json", ".msgpack": "msgpack", ".mpk": "msgpack"} def _is_gzip_path(target: Any) -> bool: return isinstance(target, (str, os.PathLike)) and str(target).lower().endswith(".gz") def _infer_format(target: Any, format: str | None) -> str: if format is not None: return format if isinstance(target, (str, os.PathLike)): suffixes = [s.lower() for s in Path(target).suffixes] if suffixes and suffixes[-1] == ".gz": suffixes = suffixes[:-1] # a .gz wraps the inner wire-format suffix suffix = suffixes[-1] if suffixes else "" return _SUFFIX_FORMATS.get(suffix, "json") return "json"
[docs] def save(obj: Any, target: Any, *, format: str | None = None) -> None: """Serialize ``obj`` to ``target``. Parameters ---------- obj : Any A PyPhi domain object with a registered serializer (see :mod:`pyphi.serialize.convert` for the supported types). target : str or os.PathLike or file object Destination path, or an open binary file object to write to. format : {"json", "msgpack"}, optional Wire format. If None (the default), it is inferred from a path's extension: ``.json`` gives ``"json"``; ``.msgpack`` or ``.mpk`` give ``"msgpack"``; any other extension, or a non-path target, gives ``"json"``. Notes ----- A path ending in ``.gz`` is gzip-compressed, with the wire format taken from the inner suffix (``result.json.gz`` yields gzip-compressed JSON). Compression is applied only to path targets, not to file objects. """ data = dumps(obj, format=_infer_format(target, format)) if isinstance(target, (str, os.PathLike)): opener = gzip.open if _is_gzip_path(target) else open with opener(target, "wb") as f: f.write(data) else: target.write(data)
[docs] def load(target: Any, *, format: str | None = None) -> Any: """Deserialize a PyPhi domain object from ``target``. Parameters ---------- target : str or os.PathLike or file object Source path, or an open binary file object to read from. format : {"json", "msgpack"}, optional Wire format. If None (the default), it is inferred from a path's extension exactly as in :func:`save`. Returns ------- Any The reconstructed PyPhi domain object. Notes ----- A path ending in ``.gz`` is transparently decompressed. """ fmt = _infer_format(target, format) if isinstance(target, (str, os.PathLike)): opener = gzip.open if _is_gzip_path(target) else open with opener(target, "rb") as f: data = f.read() else: data = target.read() return loads(data, format=fmt)