Source code for ezmsg.dashboard.backend.catalog_worker

"""Class metadata inspection; never constructs a component or its settings."""

from __future__ import annotations

import dataclasses
import inspect
import json
import sys
from pathlib import Path
from typing import TypeVar, get_args, get_origin

from .catalog import registrations


[docs] def type_label(tp: object) -> str: if isinstance(tp, type): return f"{tp.__module__}.{tp.__qualname__}" return str(tp)
[docs] def type_bindings(cls: type) -> dict: bindings = {} def walk(base: type, inherited: dict) -> None: for parent in base.__dict__.get("__orig_bases__", base.__bases__): origin = get_origin(parent) or parent args = [inherited.get(arg, arg) for arg in get_args(parent)] local = {**inherited, **dict(zip(getattr(origin, "__parameters__", ()), args))} bindings.update(local) if isinstance(origin, type) and origin is not object: walk(origin, local) walk(cls, {}) return bindings
[docs] def describe(cls: type) -> dict: import ezmsg.core as ez if not inspect.isclass(cls) or not issubclass(cls, (ez.Unit, ez.Collection)) or inspect.isabstract(cls): raise TypeError("Registration must reference a concrete ezmsg Unit or Collection class") bindings = type_bindings(cls) ports = [] for name, stream in cls.__streams__.items(): tp = bindings.get(stream.msg_type, stream.msg_type) direction = ( "input" if isinstance(stream, (ez.InputStream, ez.InputTopic, ez.InputRelay)) else "output" if isinstance(stream, (ez.OutputStream, ez.OutputTopic, ez.OutputRelay)) else "unknown" ) ports.append( { "name": name, "direction": direction, "message_type": type_label(tp), "type_resolved": not isinstance(tp, TypeVar), "settings": name == "INPUT_SETTINGS", } ) settings_type = cls.__settings_type__ fields = [] if dataclasses.is_dataclass(settings_type): for field in dataclasses.fields(settings_type): required = field.default is dataclasses.MISSING and field.default_factory is dataclasses.MISSING item = {"name": field.name, "required": required, "type": type_label(field.type)} # Do not execute default factories just to populate the catalog. if field.default is not dataclasses.MISSING: try: json.dumps(field.default, allow_nan=False) item["default"] = field.default except (TypeError, ValueError): pass fields.append(item) schema, schema_error = None, None try: from pydantic import TypeAdapter schema = TypeAdapter(settings_type).json_schema(mode="validation") except Exception as exc: schema_error = str(exc) return { "available": True, "kind": "collection" if issubclass(cls, ez.Collection) else "unit", "description": inspect.getdoc(cls) or "", "ports": ports, "settings_fields": fields, "json_schema": schema, "schema_error": schema_error, }
[docs] def main() -> None: try: matches = [ep for record, ep in registrations() if record["id"] == sys.argv[1]] if len(matches) != 1: raise ValueError("Registration is missing or ambiguous") result = describe(matches[0].load()) except BaseException as exc: result = {"available": False, "error": f"{type(exc).__name__}: {exc}"} Path(sys.argv[2]).write_text(json.dumps(result, allow_nan=False))
if __name__ == "__main__": main()