Source code for pyqt_reactive.forms.parameter_form_base
"""
Abstract base class for parameter form managers.
This module defines the common interface and shared behavior for both PyQt and Textual
parameter form implementations, establishing contracts and providing shared functionality.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Type, Optional
from dataclasses import dataclass
from pyqt_reactive.forms.parameter_form_constants import CONSTANTS
from .parameter_type_utils import ParameterTypeUtils
[docs]
@dataclass
class ParameterFormConfig:
"""
Configuration for parameter form managers.
This dataclass encapsulates all configuration options for parameter form
managers, providing a clean interface for customizing form behavior.
Attributes:
field_id: Unique identifier for the form
parameter_info: Optional parameter information dictionary
is_global_config_editing: Whether editing global configuration
global_config_type: Type of global configuration being edited
placeholder_prefix: Prefix for placeholder text
use_scroll_area: Whether to use scroll area (PyQt only)
enable_debug: Whether to enable debug logging
debug_target_params: Set of parameters to debug
framework: UI framework ('pyqt6' or 'textual')
color_scheme: Optional color scheme for PyQt
function_target: Optional function target for docstring fallback
"""
field_id: str
parameter_info: Optional[Dict] = None
is_global_config_editing: bool = False
global_config_type: Optional[Type] = None
placeholder_prefix: str = CONSTANTS.DEFAULT_PLACEHOLDER_PREFIX
use_scroll_area: Optional[bool] = None
enable_debug: bool = False
debug_target_params: Optional[set] = None
framework: str = CONSTANTS.TEXTUAL_FRAMEWORK
color_scheme: Optional[Any] = None
function_target: Optional[Any] = None
[docs]
@classmethod
def for_pyqt(cls, field_id: str, **kwargs) -> 'ParameterFormConfig':
"""Create configuration for PyQt parameter form manager."""
return cls(field_id=field_id, framework=CONSTANTS.PYQT6_FRAMEWORK, **kwargs)
[docs]
@classmethod
def for_textual(cls, field_id: str, **kwargs) -> 'ParameterFormConfig':
"""Create configuration for Textual parameter form manager."""
return cls(field_id=field_id, framework=CONSTANTS.TEXTUAL_FRAMEWORK, **kwargs)
[docs]
def with_debug(self, enabled: bool = True, target_params: Optional[set] = None) -> 'ParameterFormConfig':
"""Return a copy with debug settings configured."""
import copy
config = copy.deepcopy(self)
config.enable_debug = enabled
if target_params is not None:
config.debug_target_params = target_params
return config
[docs]
def with_global_config(self, global_config_type: Type, editing: bool = True) -> 'ParameterFormConfig':
"""Return a copy with global configuration settings."""
import copy
config = copy.deepcopy(self)
config.is_global_config_editing = editing
config.global_config_type = global_config_type
return config
[docs]
class ParameterFormManagerBase(ABC):
"""
Abstract base class for parameter form managers.
This class defines the common interface and shared behavior for both PyQt
and Textual parameter form implementations. It provides:
- Common initialization patterns
- Shared utility access
- Abstract methods that must be implemented
- Common parameter management operations
- Debug logging infrastructure
Subclasses must implement the abstract methods to provide framework-specific
widget creation and form building functionality.
"""
[docs]
def __init__(self, parameters: Dict[str, Any], parameter_types: Dict[str, Type],
config: ParameterFormConfig):
"""
Initialize the parameter form manager with common setup.
Args:
parameters: Dictionary of parameter names to current values
parameter_types: Dictionary of parameter names to types
config: Configuration object for the form manager
"""
# Store core data
self.parameters = parameters.copy()
self.parameter_types = parameter_types
self.config = config
# Initialize shared utilities
self.type_utils = ParameterTypeUtils()
# Track nested managers and widgets
self.nested_managers = {}
self.widgets = {}
# Log initialization
self.debugger.log_form_manager_operation("form_manager_initialized", {
"field_id": config.field_id,
"parameter_count": len(parameters),
"has_nested_params": any(ParameterTypeUtils.has_dataclass_fields(t) for t in parameter_types.values())
})
# Abstract methods that must be implemented by subclasses
[docs]
@abstractmethod
def build_form(self) -> Any:
"""
Build the complete form UI.
This method must be implemented by subclasses to create the framework-specific
form UI containing all parameter widgets.
Returns:
The framework-specific form widget/container
"""
pass
[docs]
@abstractmethod
def create_parameter_widget(self, param_name: str, param_type: Type, current_value: Any) -> Any:
"""
Create a widget for a single parameter.
This method must be implemented by subclasses to create framework-specific
widgets for individual parameters.
Args:
param_name: The parameter name
param_type: The parameter type
current_value: The current parameter value
Returns:
The framework-specific widget
"""
pass
[docs]
@abstractmethod
def create_nested_form(self, param_name: str, param_type: Type, current_value: Any) -> Any:
"""
Create a nested form for dataclass parameters.
This method must be implemented by subclasses to create framework-specific
nested forms for dataclass parameters.
Args:
param_name: The parameter name
param_type: The dataclass type
current_value: The current dataclass value
Returns:
The framework-specific nested form widget/container
"""
pass
[docs]
@abstractmethod
def update_widget_value(self, widget: Any, value: Any) -> None:
"""
Update a widget's value.
This method must be implemented by subclasses to update framework-specific
widget values.
Args:
widget: The framework-specific widget
value: The new value to set
"""
pass
[docs]
@abstractmethod
def get_widget_value(self, widget: Any) -> Any:
"""
Get a widget's current value.
This method must be implemented by subclasses to retrieve values from
framework-specific widgets.
Args:
widget: The framework-specific widget
Returns:
The current widget value
"""
pass
# Shared concrete methods
[docs]
def update_parameter(self, param_name: str, value: Any) -> None:
"""
Update a parameter value with type conversion and nested handling.
This method provides common parameter update logic that handles type
conversion, nested parameters, and debug logging. It updates both the
internal data model and the corresponding widget.
Args:
param_name: The parameter name to update
value: The new value
"""
self.debugger.log_parameter_update(param_name, value, "update_parameter")
# Handle nested parameters
if self._is_nested_parameter(param_name):
self._update_nested_parameter(param_name, value)
return
# Handle regular parameters
if param_name in self.parameters:
# Convert value to appropriate type
converted_value = self._convert_value_to_type(value, param_name)
# Update parameter in data model
old_value = self.parameters[param_name]
self.parameters[param_name] = converted_value
self.debugger.log_parameter_update(param_name, converted_value, "parameter_stored")
# Update corresponding widget if it exists
if param_name in self.widgets:
self.update_widget_value(self.widgets[param_name], converted_value)
[docs]
def reset_all_parameters(self, defaults: Dict[str, Any] = None) -> None:
"""
Reset all parameters to their default values.
Args:
defaults: Optional dictionary of default values to use
"""
self.debugger.log_form_manager_operation("reset_all_parameters", {
"parameter_count": len(self.parameters),
"has_custom_defaults": defaults is not None
})
# CRITICAL FIX: Iterate over a static list of keys to avoid 'dictionary changed during iteration'
param_names = list(self.parameters.keys())
for param_name in param_names:
if defaults and param_name in defaults:
default_value = defaults[param_name]
else:
default_value = self._get_default_value_for_parameter(param_name)
self.reset_parameter(param_name, default_value)
[docs]
def reset_parameter(self, param_name: str, default_value: Any = None) -> None:
"""
Reset a parameter to its default value.
Args:
param_name: The parameter name to reset
default_value: Optional default value (uses type default if None)
"""
if default_value is None:
default_value = self._get_default_value_for_parameter(param_name)
old_value = self.parameters.get(param_name)
self.debugger.log_reset_operation(param_name, old_value, default_value)
self.update_parameter(param_name, default_value)
[docs]
def get_current_values(self) -> Dict[str, Any]:
"""
Get all current parameter values.
Returns:
Dictionary of parameter names to current values
"""
return self.parameters.copy()
[docs]
def get_parameter_info(self, param_name: str) -> Optional[Any]:
"""
Get parameter information for a parameter.
Args:
param_name: The parameter name
Returns:
Parameter info object or None
"""
if self.config.parameter_info:
return self.config.parameter_info.get(param_name)
return None
# Protected helper methods
def _is_nested_parameter(self, param_name: str) -> bool:
"""Check if a parameter name represents a nested parameter."""
return CONSTANTS.FIELD_ID_SEPARATOR in param_name
def _update_nested_parameter(self, param_name: str, value: Any) -> None:
"""Update a nested parameter by delegating to the appropriate nested manager."""
parts = param_name.split(CONSTANTS.FIELD_ID_SEPARATOR)
# Find the nested manager
for i in range(1, len(parts)):
potential_nested = CONSTANTS.FIELD_ID_SEPARATOR.join(parts[:i])
if potential_nested in self.nested_managers:
nested_field = CONSTANTS.FIELD_ID_SEPARATOR.join(parts[i:])
self.debugger.log_nested_update(potential_nested, nested_field, value)
self.nested_managers[potential_nested].update_parameter(nested_field, value)
return
def _convert_value_to_type(self, value: Any, param_name: str) -> Any:
"""Convert a value to the appropriate type for a parameter."""
if param_name not in self.parameter_types or value is None:
return value
param_type = self.parameter_types[param_name]
# Handle string "None" literal
if isinstance(value, str) and value == CONSTANTS.NONE_STRING_LITERAL:
return None
# Handle enum types
if ParameterTypeUtils.is_enum_type(param_type):
return param_type(value)
# Handle list of enums
if ParameterTypeUtils.is_list_of_enums(param_type):
# If value is already a list (from checkbox group widget), return as-is
if isinstance(value, list):
return value
enum_type = ParameterTypeUtils.get_enum_from_list_type(param_type)
if enum_type:
return [enum_type(value)]
# Handle basic types
if param_type == bool and isinstance(value, str):
return ParameterTypeUtils.convert_string_to_bool(value)
if param_type in (int, float) and isinstance(value, str):
if value == CONSTANTS.EMPTY_STRING:
return None
try:
return param_type(value)
except (ValueError, TypeError):
return None
return value
def _get_default_value_for_parameter(self, param_name: str) -> Any:
"""Get the default value for a parameter."""
# This is a simplified implementation - subclasses may override
param_type = self.parameter_types.get(param_name)
if param_type == bool:
return False
elif param_type == int:
return 0
elif param_type == float:
return 0.0
elif param_type == str:
return CONSTANTS.EMPTY_STRING
else:
return None