Source code for pyqt_reactive.forms.widget_factory
"""
Widget factory with explicit type-based dispatch.
Replaces duck typing with fail-loud type checking.
Mirrors MemoryType converter pattern - explicit type → widget mapping.
Design:
- WIDGET_TYPE_REGISTRY: Type → factory function mapping
- Explicit dispatch (no hasattr checks)
- Fail-loud if type not registered
- Handles Optional[T], Enum, List[Enum] types
"""
from typing import Type, Any, Dict, Callable, get_origin, get_args, Union
from enum import Enum
import logging
logger = logging.getLogger(__name__)
# Type-based widget creation dispatch - NO DUCK TYPING
# Maps Python type → widget factory function
WIDGET_TYPE_REGISTRY: Dict[Type, Callable] = {}
def _init_widget_type_registry():
"""
Initialize widget type registry with Qt adapters.
Lazy initialization to avoid import errors when PyQt6 not available.
"""
global WIDGET_TYPE_REGISTRY
if WIDGET_TYPE_REGISTRY:
# Already initialized
return
try:
from pyqt_reactive.protocols.widget_adapters import (
LineEditAdapter, SpinBoxAdapter, DoubleSpinBoxAdapter,
ComboBoxAdapter, CheckBoxAdapter
)
WIDGET_TYPE_REGISTRY.update({
str: lambda: LineEditAdapter(),
int: lambda: SpinBoxAdapter(),
float: lambda: DoubleSpinBoxAdapter(),
bool: lambda: CheckBoxAdapter(),
})
logger.debug("Initialized WIDGET_TYPE_REGISTRY with Qt adapters")
except ImportError as e:
logger.warning(f"Could not initialize Qt widget adapters: {e}")
[docs]
def resolve_optional(param_type: Type) -> Type:
"""
Resolve Optional[T] to T.
Args:
param_type: Type to resolve (e.g., Optional[int])
Returns:
Unwrapped type (e.g., int)
"""
if get_origin(param_type) is Union:
args = get_args(param_type)
if len(args) == 2 and type(None) in args:
return next(arg for arg in args if arg is not type(None))
return param_type
[docs]
def is_enum_type(param_type: Type) -> bool:
"""
Check if type is an Enum.
Args:
param_type: Type to check
Returns:
True if param_type is an Enum subclass
"""
return isinstance(param_type, type) and issubclass(param_type, Enum)
[docs]
def is_list_of_enums(param_type: Type) -> bool:
"""
Check if type is List[Enum].
Args:
param_type: Type to check
Returns:
True if param_type is List[SomeEnum]
"""
if get_origin(param_type) is list:
args = get_args(param_type)
if args and is_enum_type(args[0]):
return True
return False
[docs]
def get_enum_from_list(param_type: Type) -> Type:
"""
Extract enum type from List[Enum].
Args:
param_type: List[Enum] type
Returns:
The Enum type
"""
return get_args(param_type)[0]
[docs]
class WidgetFactory:
"""
Widget factory using explicit type-based dispatch.
Replaces duck typing with fail-loud type checking.
Mirrors the pattern from MemoryType converters.
Example:
factory = WidgetFactory()
# Create widget for int parameter
widget = factory.create_widget(int, "my_param")
# Returns SpinBoxAdapter instance
# Create widget for Enum parameter
widget = factory.create_widget(MyEnum, "mode")
# Returns ComboBoxAdapter populated with enum values
"""
[docs]
def __init__(self):
"""Initialize factory and ensure registry is populated."""
_init_widget_type_registry()
[docs]
def create_widget(self, param_type: Type, param_name: str = "") -> Any:
"""
Create widget for parameter type using explicit dispatch.
Args:
param_type: The parameter type to create widget for
param_name: Optional parameter name for debugging
Returns:
Widget instance implementing required ABCs
Raises:
TypeError: If no widget registered for this type
Example:
>>> factory = WidgetFactory()
>>> widget = factory.create_widget(int, "threshold")
>>> isinstance(widget, SpinBoxAdapter)
True
"""
# Handle Optional[T] by unwrapping
original_type = param_type
param_type = resolve_optional(param_type)
# Handle Enum types
if is_enum_type(param_type):
return self._create_enum_widget(param_type)
# Handle List[Enum] types
if is_list_of_enums(param_type):
enum_type = get_enum_from_list(param_type)
return self._create_enum_list_widget(enum_type)
# Explicit type dispatch - FAIL LOUD if type not registered
factory_func = WIDGET_TYPE_REGISTRY.get(param_type)
if factory_func is None:
raise TypeError(
f"No widget registered for type {param_type} (parameter: '{param_name}'). "
f"Available types: {list(WIDGET_TYPE_REGISTRY.keys())}. "
f"Add widget factory to WIDGET_TYPE_REGISTRY or create custom adapter."
)
widget = factory_func()
logger.debug(f"Created {type(widget).__name__} for parameter '{param_name}' (type: {param_type})")
return widget
def _create_enum_widget(self, enum_type: Type) -> Any:
"""
Create ComboBox widget for Enum type.
Args:
enum_type: The Enum class
Returns:
ComboBoxAdapter populated with enum values
"""
from pyqt_reactive.protocols.widget_adapters import ComboBoxAdapter
widget = ComboBoxAdapter()
widget.populate_enum(enum_type)
logger.debug(f"Created ComboBoxAdapter for enum {enum_type.__name__}")
return widget
def _create_enum_list_widget(self, enum_type: Type) -> Any:
"""
Create multi-select widget for List[Enum] type.
Args:
enum_type: The Enum class
Returns:
Multi-select widget (TODO: implement EnumMultiSelectAdapter)
Note:
Currently falls back to single-select ComboBox.
Future: Implement proper multi-select widget.
"""
# TODO: Implement EnumMultiSelectAdapter for List[Enum]
# For now, fall back to single-select
logger.warning(
f"List[{enum_type.__name__}] not fully supported yet. "
f"Using single-select ComboBox as fallback."
)
return self._create_enum_widget(enum_type)
[docs]
def register_widget_type(self, param_type: Type, factory_func: Callable) -> None:
"""
Register custom widget factory for a type.
Args:
param_type: The Python type to register
factory_func: Function that creates widget instance (no args)
Example:
>>> def create_path_widget():
... return PathSelectorWidget()
>>> factory = WidgetFactory()
>>> factory.register_widget_type(Path, create_path_widget)
"""
if param_type in WIDGET_TYPE_REGISTRY:
logger.warning(
f"Overwriting existing widget factory for type {param_type}"
)
WIDGET_TYPE_REGISTRY[param_type] = factory_func
logger.debug(f"Registered widget factory for type {param_type}")