Source code for pyqt_reactive.forms.widget_operations

"""
Centralized widget operations using ABC-based dispatch.

Replaces scattered duck typing with explicit ABC checks.
Eliminates WIDGET_UPDATE_DISPATCH and WIDGET_GET_DISPATCH tables.

Design:
- Single source of truth for widget operations
- ABC-based dispatch (no hasattr checks)
- Fail-loud on missing implementations
- Discoverable via registry
"""

from typing import Any, Callable
from .widget_dispatcher import WidgetDispatcher
from pyqt_reactive.protocols import (
    ValueGettable, ValueSettable, PlaceholderCapable,
    RangeConfigurable, ChangeSignalEmitter
)
from .widget_registry import WIDGET_IMPLEMENTATIONS


[docs] class WidgetOperations: """ Centralized widget operations using ABC-based dispatch. Replaces scattered duck typing with explicit ABC checks. Eliminates WIDGET_UPDATE_DISPATCH and WIDGET_GET_DISPATCH. Example: ops = WidgetOperations() # Get value (fails loud if widget doesn't implement ValueGettable) value = ops.get_value(widget) # Set value (fails loud if widget doesn't implement ValueSettable) ops.set_value(widget, 42) # Set placeholder (fails loud if widget doesn't implement PlaceholderCapable) ops.set_placeholder(widget, "Pipeline default: 100") """
[docs] @staticmethod def get_value(widget: Any) -> Any: """ Get value from any widget implementing ValueGettable. Args: widget: The widget to get value from Returns: The widget's current value Raises: TypeError: If widget doesn't implement ValueGettable ABC """ return WidgetDispatcher.get_value(widget)
[docs] @staticmethod def set_value(widget: Any, value: Any) -> None: """ Set value on any widget implementing ValueSettable. Args: widget: The widget to set value on value: The value to set Raises: TypeError: If widget doesn't implement ValueSettable ABC """ WidgetDispatcher.set_value(widget, value)
[docs] @staticmethod def set_placeholder(widget: Any, text: str) -> None: """ Set placeholder on any widget implementing PlaceholderCapable. Args: widget: The widget to set placeholder on text: The placeholder text Raises: TypeError: If widget doesn't implement PlaceholderCapable ABC """ WidgetDispatcher.set_placeholder(widget, text)
[docs] @staticmethod def configure_range(widget: Any, minimum: float, maximum: float) -> None: """ Configure range on any widget implementing RangeConfigurable. Args: widget: The widget to configure minimum: Minimum value maximum: Maximum value Raises: TypeError: If widget doesn't implement RangeConfigurable ABC """ WidgetDispatcher.configure_range(widget, minimum, maximum)
[docs] @staticmethod def connect_change_signal(widget: Any, callback: Callable[[Any], None]) -> None: """ Connect change signal on any widget implementing ChangeSignalEmitter. Args: widget: The widget to connect signal on callback: Callback function receiving new value Raises: TypeError: If widget doesn't implement ChangeSignalEmitter ABC """ WidgetDispatcher.connect_change_signal(widget, callback)
[docs] @staticmethod def disconnect_change_signal(widget: Any, callback: Callable[[Any], None]) -> None: """ Disconnect change signal on any widget implementing ChangeSignalEmitter. Args: widget: The widget to disconnect signal from callback: Callback function to disconnect Raises: TypeError: If widget doesn't implement ChangeSignalEmitter ABC """ WidgetDispatcher.disconnect_change_signal(widget, callback)
[docs] @staticmethod def get_all_value_widgets(container: Any) -> list: """ Get all widgets that implement ValueGettable ABC. Replaces findChildren() with explicit type lists. Uses ABC checking instead of duck typing. Args: container: The container widget to search in Returns: List of widgets implementing ValueGettable Example: >>> ops = WidgetOperations() >>> form = MyFormWidget() >>> value_widgets = ops.get_all_value_widgets(form) >>> values = {w.objectName(): ops.get_value(w) for w in value_widgets} """ # Start with registered widget types widget_types = tuple(WIDGET_IMPLEMENTATIONS.values()) collected = [] if widget_types: collected.extend(container.findChildren(widget_types)) # Fallback: also include any child that declares ValueGettable via ABC # (e.g., NoneAwareLineEdit/CheckBox which are registered virtually, not in WIDGET_IMPLEMENTATIONS) try: from PyQt6.QtCore import QObject for widget in container.findChildren(QObject): if isinstance(widget, ValueGettable): collected.append(widget) except Exception: # If PyQt isn't available in a non-GUI context, gracefully return what we have pass # Deduplicate while preserving order seen_ids = set() value_widgets = [] for widget in collected: wid = id(widget) if wid in seen_ids: continue seen_ids.add(wid) if isinstance(widget, ValueGettable): value_widgets.append(widget) return value_widgets
[docs] @staticmethod def try_set_placeholder(widget: Any, text: str) -> bool: """ Try to set placeholder, return False if widget doesn't support it. This is the ONLY acceptable use of "try" pattern - when placeholder support is truly optional and we want to gracefully skip widgets that don't support it. Args: widget: The widget to set placeholder on text: The placeholder text Returns: True if placeholder was set, False if widget doesn't support it """ if not isinstance(widget, PlaceholderCapable): return False try: widget.set_placeholder(text) return True except Exception: # Unexpected error - log but don't crash import logging logging.getLogger(__name__).warning( f"Failed to set placeholder on {type(widget).__name__}: {text}", exc_info=True ) return False
[docs] @staticmethod def try_configure_range(widget: Any, minimum: float, maximum: float) -> bool: """ Try to configure range, return False if widget doesn't support it. Similar to try_set_placeholder - acceptable for optional configuration. Args: widget: The widget to configure minimum: Minimum value maximum: Maximum value Returns: True if range was configured, False if widget doesn't support it """ if not isinstance(widget, RangeConfigurable): return False try: widget.configure_range(minimum, maximum) return True except Exception: import logging logging.getLogger(__name__).warning( f"Failed to configure range on {type(widget).__name__}: [{minimum}, {maximum}]", exc_info=True ) return False