Step Editor Generalization Architecture ======================================= Generic step editor patterns that automatically adapt to AbstractStep constructor changes, eliminating hardcoded parameter handling through type-based discovery and automatic configuration. Overview -------- The step editor generalization system solves a fundamental problem in UI development: how do you create editors that automatically adapt when the AbstractStep constructor signature changes? OpenHCS has one step type (FunctionStep) that inherits from AbstractStep. The AbstractStep constructor may change to have different configs or additional parameters in the future. Traditional approaches require manual mapping of each parameter type to UI widgets, creating maintenance overhead when the constructor evolves. A generic system uses type introspection to automatically detect AbstractStep constructor parameters, create appropriate UI widgets, and establish inheritance relationships with pipeline configuration. The system adapts automatically when the constructor signature changes. This eliminated hardcoded parameter handling, reduced step editor code, and enabled automatic adaptation to AbstractStep constructor changes without code modifications. Generic Step Editor Patterns ----------------------------- The system uses several key patterns to achieve complete generalization. Automatic Parameter Detection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The step editor automatically analyzes the AbstractStep constructor to extract parameter information: .. code-block:: python # Automatic parameter analysis of AbstractStep constructor param_info = SignatureAnalyzer.analyze(AbstractStep.__init__) # Extract all parameters with type information parameters = {} parameter_types = {} param_defaults = {} for name, info in param_info.items(): current_value = getattr(step, name, info.default_value) parameters[name] = current_value parameter_types[name] = info.param_type param_defaults[name] = info.default_value **SignatureAnalyzer Capabilities:** .. code-block:: python class SignatureAnalyzer: @staticmethod def analyze(target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]: """Extract parameter information from any target.""" # Handles multiple target types: if inspect.isclass(target): if dataclasses.is_dataclass(target): return SignatureAnalyzer._analyze_dataclass(target) else: return SignatureAnalyzer._analyze_callable(target.__init__) elif dataclasses.is_dataclass(target): return SignatureAnalyzer._analyze_dataclass_instance(target) else: return SignatureAnalyzer._analyze_callable(target) Type-Based Parameter Classification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Parameters are automatically classified based on their type annotations: .. code-block:: python def _classify_parameter(self, param_type: Type, param_name: str) -> ParameterClassification: """Classify parameter based on type annotation.""" # Optional dataclass parameters if ParameterTypeUtils.is_optional_dataclass(param_type): inner_type = ParameterTypeUtils.get_optional_inner_type(param_type) return ParameterClassification( category="optional_dataclass", inner_type=inner_type, requires_checkbox=True, supports_inheritance=self._has_pipeline_mapping(inner_type) ) # Regular dataclass parameters elif dataclasses.is_dataclass(param_type): return ParameterClassification( category="nested_dataclass", inner_type=param_type, requires_checkbox=False, supports_inheritance=False ) # Primitive parameters else: return ParameterClassification( category="primitive", widget_type=self._determine_widget_type(param_type) ) Current AbstractStep Parameter Handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The system handles the current AbstractStep constructor signature automatically: .. code-block:: python # Current AbstractStep constructor (as of this documentation): class AbstractStep(abc.ABC): def __init__( self, *, # Force keyword-only arguments name: Optional[str] = None, variable_components: List[VariableComponents] = DEFAULT_VARIABLE_COMPONENTS, group_by: Optional[GroupBy] = DEFAULT_GROUP_BY, __input_dir__: Optional[Union[str,Path]] = None, # Internal __output_dir__: Optional[Union[str,Path]] = None, # Internal input_source: InputSource = InputSource.PREVIOUS_STEP, materialization_config: Optional['LazyStepMaterializationConfig'] = None ) -> None: # Automatically detected parameters: # - name: Optional[str] → Text input widget # - variable_components: List[VariableComponents] → Multi-select widget # - group_by: Optional[GroupBy] → Dropdown widget # - input_source: InputSource → Radio button widget # - materialization_config: Optional[LazyStepMaterializationConfig] → # Checkbox + nested form with pipeline inheritance **Automatic Parameter Processing:** .. code-block:: python # Works with current AbstractStep constructor and adapts to changes for name, info in param_info.items(): # Generic handling based on type classification if self._is_optional_lazy_dataclass_in_pipeline(info.param_type, name): # Automatic step-level config creation (e.g., materialization_config) step_level_config = self._create_step_level_config(name, info.param_type) current_value = step_level_config else: # Standard parameter handling (e.g., name, variable_components) current_value = getattr(step, name, info.default_value) parameters[name] = current_value parameter_types[name] = info.param_type Evolution-Proof Implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The system eliminates hardcoded parameter mappings to adapt automatically when AbstractStep constructor changes: **Before (Hardcoded Approach):** .. code-block:: python # Manual mapping that breaks when AbstractStep constructor changes if param_name == "materialization_config": return self._create_materialization_widget() elif param_name == "variable_components": return self._create_variable_components_widget() elif param_name == "name": return self._create_string_widget() # ... breaks when new parameters added to AbstractStep **After (Type-Based Discovery):** .. code-block:: python # Automatic widget creation based on type annotations widget_type = self._classify_parameter_type(param_info.param_type) if widget_type == ParameterType.OPTIONAL_DATACLASS: return self._create_optional_dataclass_widget(param_info) elif widget_type == ParameterType.ENUM: return self._create_enum_widget(param_info) elif widget_type == ParameterType.PRIMITIVE: return self._create_primitive_widget(param_info) # ... adapts automatically to AbstractStep constructor changes # 2. Get inner dataclass type inner_type = ParameterTypeUtils.get_optional_inner_type(param_type) # 3. Find if this type exists in PipelineConfig (type-based matching) pipeline_field_name = self._find_pipeline_field_by_type(inner_type) return pipeline_field_name is not None **Type-Based Discovery:** .. code-block:: python def _find_pipeline_field_by_type(self, target_type): """Find pipeline field by type - no manual mappings.""" from openhcs.core.pipeline_config import PipelineConfig for field in dataclasses.fields(PipelineConfig): # Type-based matching eliminates hardcoded field names if str(field.type) == str(target_type): return field.name return None Optional Lazy Dataclass Handling --------------------------------- The system provides sophisticated handling for optional dataclass parameters with checkbox controls and inheritance. Checkbox and Placeholder Logic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Optional dataclass parameters get automatic checkbox controls that enable/disable the parameter: .. code-block:: python # Automatic checkbox creation for Optional[dataclass] parameters def _create_optional_dataclass_widget(self, param_info): """Create checkbox + form widget for optional dataclass.""" # Checkbox controls whether parameter is enabled checkbox = self._create_checkbox( f"{param_info.name}_enabled", f"Enable {param_info.display_name}", param_info.current_value is not None ) # Form widget shows when checkbox is enabled form_widget = self._create_nested_form(param_info) # Placeholder text shows inheritance chain value placeholder_text = self._get_inheritance_placeholder(param_info) form_widget.setPlaceholderText(placeholder_text) return checkbox, form_widget **Checkbox State Management:** .. code-block:: python def handle_optional_checkbox_change(self, param_name: str, enabled: bool): """Handle checkbox state changes.""" if enabled: # Create default instance when enabled param_type = self.parameter_types[param_name] inner_type = ParameterTypeUtils.get_optional_inner_type(param_type) default_instance = inner_type() self.update_parameter(param_name, default_instance) else: # Set to None when disabled (enables inheritance) self.update_parameter(param_name, None) Automatic Step-Level Config Creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When an optional lazy dataclass parameter is detected, the system automatically creates step-level configuration: .. code-block:: python def _create_step_level_config(self, param_name, param_type): """Generic step-level config creation for any lazy dataclass.""" # Get inner dataclass type inner_type = ParameterTypeUtils.get_optional_inner_type(param_type) # Find corresponding pipeline field by type (no hardcoding) pipeline_field_name = self._find_pipeline_field_by_type(inner_type) if not pipeline_field_name: return inner_type() # Fallback to standard config # Get pipeline field as defaults source pipeline_config = get_current_global_config(GlobalPipelineConfig) if pipeline_config and hasattr(pipeline_config, pipeline_field_name): pipeline_field_value = getattr(pipeline_config, pipeline_field_name) # Create step-level config with inheritance StepLevelConfig = LazyDataclassFactory.create_lazy_dataclass( defaults_source=pipeline_field_value, lazy_class_name=f"StepLevel{inner_type.__name__}", use_recursive_resolution=False ) return StepLevelConfig() return inner_type() Parameter-to-Pipeline-Field Mapping ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The system automatically maps step parameters to pipeline configuration fields using type-based discovery: .. code-block:: python # Automatic mapping examples: # Step parameter: materialization_config: Optional[StepMaterializationConfig] # Maps to: pipeline.materialization_defaults (type: StepMaterializationConfig) # Step parameter: vfs_config: Optional[VFSConfig] # Maps to: pipeline.vfs (type: VFSConfig) # Step parameter: analysis_config: Optional[AnalysisConfig] # Maps to: pipeline.analysis_defaults (type: AnalysisConfig) **Mapping Algorithm:** .. code-block:: python def _establish_parameter_mapping(self, step_params, pipeline_config_type): """Establish automatic parameter-to-pipeline mappings.""" mappings = {} for param_name, param_type in step_params.items(): if ParameterTypeUtils.is_optional_dataclass(param_type): inner_type = ParameterTypeUtils.get_optional_inner_type(param_type) # Find pipeline field with matching type pipeline_field = self._find_pipeline_field_by_type(inner_type) if pipeline_field: mappings[param_name] = { 'pipeline_field': pipeline_field, 'inheritance_enabled': True, 'step_level_config': True } return mappings Real-World Usage Example ------------------------ This example shows how the system handles the actual FunctionStep constructor automatically. Current FunctionStep Usage ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Creating a FunctionStep (the only step type in OpenHCS) from openhcs.core.steps.function_step import FunctionStep from openhcs.processing.backends.processors.cupy_processor import tophat from openhcs.constants.constants import VariableComponents step = FunctionStep( func=tophat, name="morphological_opening", variable_components=[VariableComponents.CHANNEL], materialization_config=None # Will be handled by step editor ) # Step editor automatically detects AbstractStep parameters: # - name: Optional[str] → Text input widget # - variable_components: List[VariableComponents] → Multi-select widget # - group_by: Optional[GroupBy] → Dropdown widget # - input_source: InputSource → Radio button widget # - materialization_config: Optional[LazyStepMaterializationConfig] → # Checkbox + nested form with pipeline inheritance Future Evolution Scenarios ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The system is designed to handle potential AbstractStep constructor changes automatically: .. code-block:: python # Hypothetical future AbstractStep constructor changes: # Scenario 1: New optional config parameter added class AbstractStep(abc.ABC): def __init__(self, # ... existing parameters ... analysis_config: Optional[AnalysisConfig] = None): # NEW # Step editor automatically detects and creates: # - Checkbox: "Enable Analysis Config" # - Nested form with pipeline inheritance # - No code changes required # Scenario 2: Parameter type changed class AbstractStep(abc.ABC): def __init__(self, # ... existing parameters ... variable_components: VariableComponents = VariableComponents.SITE): # Changed from List # Step editor automatically adapts: # - Changes from multi-select to single dropdown # - No manual widget mapping updates needed Actual Implementation Example ---------------------------- This example shows how the step editor actually works with the current FunctionStep. Real Step Editor Implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Actual step editor implementation (simplified) class StepParameterEditor: """Step editor that handles AbstractStep parameters automatically.""" def __init__(self, step: FunctionStep): self.step = step # Analyze AbstractStep constructor automatically from openhcs.textual_tui.widgets.shared.signature_analyzer import SignatureAnalyzer from openhcs.core.steps.abstract import AbstractStep param_info = SignatureAnalyzer.analyze(AbstractStep.__init__) # Extract current parameter values self.parameters = {} self.parameter_types = {} for name, info in param_info.items(): current_value = getattr(self.step, name, info.default_value) self.parameters[name] = current_value self.parameter_types[name] = info.param_type # Create parameter form manager for UI generation from openhcs.ui.shared.parameter_form_service import ParameterFormService self.service = ParameterFormService() def build_form(self): """Build step parameter form automatically.""" # Service layer analyzes parameters and creates form structure form_structure = self.service.analyze_parameters( self.parameters, self.parameter_types, "step_editor" ) # Create widgets based on parameter types widgets = [] for param_info in form_structure.parameters: if param_info.is_optional and param_info.is_nested: # Optional dataclass → checkbox + nested form widget = self._create_optional_dataclass_widget(param_info) elif param_info.param_type == str: # String → text input widget = self._create_text_widget(param_info) elif hasattr(param_info.param_type, '__bases__') and Enum in param_info.param_type.__bases__: # Enum → dropdown or radio buttons widget = self._create_enum_widget(param_info) # ... automatic widget creation for all parameter types widgets.append(widget) return widgets Architectural Impact -------------------- The step editor generalization system provides a foundation for maintainable UI development in OpenHCS: **Evolution Preparedness** The system automatically adapts when AbstractStep constructor changes, eliminating the need for manual UI updates and reducing maintenance overhead. **Type-Safe UI Generation** By using actual Python type annotations rather than manual mappings, the system prevents configuration errors and provides compile-time validation. **Framework Independence** The same parameter analysis logic works across both PyQt6 and Textual frameworks, ensuring consistent behavior and reducing code duplication. **Configuration Integration** Automatic detection and handling of lazy dataclass parameters enables sophisticated configuration inheritance without hardcoded mappings. This architecture ensures that OpenHCS UI development remains maintainable and extensible as the system evolves. See Also -------- - :doc:`configuration_framework` - Configuration system architecture that enables zero-hardcoding - :doc:`service-layer-architecture` - Framework-agnostic service patterns used in step editors - :doc:`code_ui_interconversion` - Code/UI interconversion patterns