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:
# 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:
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:
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:
# 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:
# 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):
# 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):
# 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:
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:
# 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:
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:
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:
# 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:
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
# 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:
# 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
# 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
Configuration Framework - Configuration system architecture that enables zero-hardcoding
Service Layer Architecture - Framework-agnostic service patterns used in step editors
Code/UI Interconversion System - Code/UI interconversion patterns