Source code for pyqt_reactive.forms.widget_registry

"""
Widget registry with metaclass auto-registration.

Mirrors StorageBackendMeta pattern - widgets auto-register when their classes
are defined, eliminating manual registration boilerplate.

Design:
- WidgetMeta metaclass handles auto-registration
- WIDGET_IMPLEMENTATIONS: Global registry of all widget types
- WIDGET_CAPABILITIES: Tracks which ABCs each widget implements
- Fail-loud if widget missing _widget_id or abstract methods
"""

from abc import ABCMeta
from typing import Dict, Type, Set
import logging

logger = logging.getLogger(__name__)

# Global registry of widget implementations
# Maps widget_id -> widget class
WIDGET_IMPLEMENTATIONS: Dict[str, Type] = {}

# Track which ABCs each widget implements
# Maps widget class -> set of ABC classes
WIDGET_CAPABILITIES: Dict[Type, Set[Type]] = {}


[docs] class WidgetMeta(ABCMeta): """ Metaclass for automatic widget registration. Mirrors StorageBackendMeta pattern: 1. Only registers concrete implementations (no abstract methods) 2. Requires _widget_id attribute for identification 3. Auto-populates WIDGET_IMPLEMENTATIONS registry 4. Tracks capabilities (which ABCs implemented) Example: class LineEditAdapter(QLineEdit, ValueGettable, ValueSettable, metaclass=WidgetMeta): _widget_id = "line_edit" def get_value(self) -> Any: return self.text() def set_value(self, value: Any) -> None: self.setText(str(value) if value is not None else "") The widget auto-registers in WIDGET_IMPLEMENTATIONS["line_edit"] when the class is defined. """ def __new__(cls, name, bases, attrs): new_class = super().__new__(cls, name, bases, attrs) # Only register concrete implementations (no abstract methods remaining) if not new_class.__abstractmethods__: # Only concrete classes that declare their widget id participate in registration. widget_id = attrs.get("_widget_id") if widget_id is None: # No _widget_id - skip registration (might be intermediate base class) logger.debug(f"Skipping registration for {name} - no _widget_id attribute") return new_class # Check for duplicate registration if widget_id in WIDGET_IMPLEMENTATIONS: existing = WIDGET_IMPLEMENTATIONS[widget_id] logger.warning( f"Widget ID '{widget_id}' already registered to {existing.__name__}. " f"Overwriting with {name}." ) # Auto-register in global registry WIDGET_IMPLEMENTATIONS[widget_id] = new_class # Track capabilities (which ABCs this widget implements) capabilities = set() # Import ABCs to check against from pyqt_reactive.protocols import ( ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, EnumSelectable, ChangeSignalEmitter ) abc_types = { ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, EnumSelectable, ChangeSignalEmitter } # Check which ABCs this widget implements for abc_type in abc_types: if issubclass(new_class, abc_type): capabilities.add(abc_type) WIDGET_CAPABILITIES[new_class] = capabilities logger.debug( f"Auto-registered {name} as '{widget_id}' with capabilities: " f"{[c.__name__ for c in capabilities]}" ) else: # Abstract class - log for debugging logger.debug( f"Skipping registration for {name} - abstract methods remaining: " f"{new_class.__abstractmethods__}" ) return new_class
[docs] def get_widget_class(widget_id: str) -> Type: """ Get widget class by ID. Args: widget_id: The widget identifier (e.g., "line_edit") Returns: The widget class Raises: KeyError: If widget_id not registered """ if widget_id not in WIDGET_IMPLEMENTATIONS: raise KeyError( f"No widget registered with ID '{widget_id}'. " f"Available widgets: {list(WIDGET_IMPLEMENTATIONS.keys())}" ) return WIDGET_IMPLEMENTATIONS[widget_id]
[docs] def get_widget_capabilities(widget_class: Type) -> Set[Type]: """ Get the ABCs that a widget class implements. Args: widget_class: The widget class to query Returns: Set of ABC classes the widget implements """ return WIDGET_CAPABILITIES.get(widget_class, set())
[docs] def list_widgets_with_capability(capability: Type) -> list[Type]: """ Find all widgets that implement a specific ABC. Args: capability: The ABC class to search for (e.g., ValueGettable) Returns: List of widget classes implementing the ABC Example: >>> from pyqt_reactive.protocols import PlaceholderCapable >>> widgets = list_widgets_with_capability(PlaceholderCapable) >>> print([w.__name__ for w in widgets]) ['LineEditAdapter', 'SpinBoxAdapter', 'ComboBoxAdapter'] """ return [ widget_class for widget_class, capabilities in WIDGET_CAPABILITIES.items() if capability in capabilities ]