from __future__ import annotations import logging import threading from typing import Any, Iterator from yuxi.channel.protocols import MetaProtocol logger = logging.getLogger(__name__) class _RegistryMeta(type): def __len__(cls) -> int: return len(cls._plugins) def __iter__(cls) -> Iterator[Any]: return iter( sorted( cls._plugins.values(), key=lambda p: (getattr(p, "order", 0), getattr(p, "id", "")), ) ) def __contains__(cls, plugin_id: str) -> bool: return plugin_id in cls._plugins def __repr__(cls) -> str: if not cls._plugins: return "" ids = sorted(cls._plugins.keys()) preview = ", ".join(ids[:5]) tail = "..." if len(ids) > 5 else "" return f"" class ChannelPluginRegistry(metaclass=_RegistryMeta): _plugins: dict[str, Any] = {} _lock: threading.Lock | None = None @classmethod def _ensure_lock(cls) -> threading.Lock: if cls._lock is None: cls._lock = threading.Lock() return cls._lock @classmethod def register(cls, plugin: Any) -> Any: if not isinstance(plugin, MetaProtocol): raise ValueError("Channel plugin must satisfy MetaProtocol (id, name, order)") plugin_id = getattr(plugin, "id", None) if not plugin_id or not isinstance(plugin_id, str) or not plugin_id.strip(): raise ValueError("Channel plugin must have a non-empty string 'id' attribute") with cls._ensure_lock(): if plugin_id in cls._plugins: raise ValueError(f"Channel plugin '{plugin_id}' is already registered") cls._plugins[plugin_id] = plugin logger.info("Registered channel plugin: %s", plugin_id) return plugin @classmethod def get(cls, plugin_id: str) -> Any: return cls._plugins.get(plugin_id) @classmethod def list_plugins(cls) -> list[str]: return cls.list_ids() @classmethod def list_all(cls) -> list[Any]: return sorted( cls._plugins.values(), key=lambda p: (getattr(p, "order", 0), getattr(p, "id", "")), ) @classmethod def list_ids(cls) -> list[str]: return sorted(cls._plugins.keys()) @classmethod def all(cls) -> list[Any]: return cls.list_all() @classmethod def unregister(cls, plugin_id: str) -> Any | None: with cls._ensure_lock(): plugin = cls._plugins.pop(plugin_id, None) if plugin is not None: logger.info("Unregistered channel plugin: %s", plugin_id) return plugin @classmethod def retire(cls, plugin_id: str) -> Any | None: return cls.unregister(plugin_id) @classmethod def clear(cls) -> None: with cls._ensure_lock(): cls._plugins.clear() @classmethod def has(cls, plugin_id: str) -> bool: return plugin_id in cls._plugins @classmethod def is_registered(cls, plugin_id: str) -> bool: return cls.has(plugin_id)