Source code for q2m3.sqd.transport

"""Picklable encoding of frozen SQD records for cross-process transport."""

from __future__ import annotations

import threading
from collections.abc import Mapping
from dataclasses import fields, is_dataclass
from importlib import import_module
from typing import Any

from q2m3.sqd.exceptions import ReferenceTimeoutError

_RECORD_PREFIX = "q2m3.sqd."


[docs] def encode_result(value: Any) -> tuple: """Encode a value as nested tagged tuples that pickle without custom reducers. Dataclass records become ``("record", module, name, fields)``. Mappings, including read-only snapshots, become plain dictionaries, so the record constructor must freeze them again on decode. Args: value: A dataclass record, mapping, tuple, list or plain value. Returns: The tagged encoding of ``value``. """ if is_dataclass(value) and not isinstance(value, type): return ( "record", type(value).__module__, type(value).__name__, {f.name: encode_result(getattr(value, f.name)) for f in fields(value)}, ) if isinstance(value, Mapping): return ("mapping", {k: encode_result(v) for k, v in value.items()}) if isinstance(value, tuple): return ("tuple", tuple(encode_result(v) for v in value)) if isinstance(value, list): return ("list", [encode_result(v) for v in value]) return ("value", value)
[docs] def decode_result(value: tuple, *, cancelled: threading.Event | None = None) -> Any: """Rebuild a value encoded by :func:`encode_result`. Records are rebuilt through their constructors, so their validation runs again. Only record types defined under ``q2m3.sqd.`` are rebuilt. This limits which types can be constructed; it does not make untrusted payloads safe. Callers must only unpickle payloads that the parent process wrote to its own private directory or pipe. ``SQDConfig`` cannot be decoded: its ``molecule`` field is a ``q2m3.molecule.MoleculeConfig``. Args: value: The tagged encoding. cancelled: Flag checked before each node is rebuilt. Returns: The rebuilt value. Raises: ReferenceTimeoutError: If ``cancelled`` is set during reconstruction. ValueError: If a record type is defined outside ``q2m3.sqd.``. """ if cancelled is not None and cancelled.is_set(): raise ReferenceTimeoutError("SQD result reconstruction cancelled") kind, *parts = value if kind == "record": module, name, values = parts if not module.startswith(_RECORD_PREFIX): raise ValueError(f"Refusing to rebuild record {module}.{name} outside q2m3.sqd") return getattr(import_module(module), name)( **{k: decode_result(v, cancelled=cancelled) for k, v in values.items()} ) if kind == "mapping": return {k: decode_result(v, cancelled=cancelled) for k, v in parts[0].items()} if kind in ("tuple", "list"): values = [decode_result(v, cancelled=cancelled) for v in parts[0]] return tuple(values) if kind == "tuple" else values return parts[0]