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
]