Placeholder Inheritance Debugging Guide
Systematic debugging approaches for the simplified OpenHCS placeholder inheritance system.
Status: STABLE - Simplified Implementation Module: openhcs.pyqt_gui.widgets.shared.parameter_form_manager
Overview
The simplified placeholder inheritance system enables configuration fields to inherit values from parent configurations through contextvars-based lazy dataclass resolution. When users click reset buttons on inherited fields, the system uses explicit context management to allow proper inheritance resolution. This guide provides systematic debugging approaches for inheritance chain failures.
The system now uses explicit contextvars-based context management, eliminating complex context discovery and frame injection mechanisms while maintaining full inheritance functionality.
Understanding the inheritance flow is essential for debugging: child fields with None values trigger lazy resolution, which checks thread-local context for parent field values, then generates placeholder text showing inherited values like “Pipeline default: {inherited_value}”.
System Architecture
Core Components
Form Managers: UI components that manage individual configuration sections. Each form manager has a field_id that identifies its configuration section and manages widgets for that section’s parameters.
Context Building: Process of creating configuration context for inheritance resolution by collecting current values from all form managers and combining widget state with parameter values.
Exclusion Logic: Mechanism to exclude specific fields during reset operations, implemented in the widget reading loop of _build_context_from_current_form_values().
Lazy Resolution: On-demand value resolution that checks thread-local context for parent field values and falls back to defaults when inheritance chains are available.
Field Naming Architecture
The system handles two distinct field naming patterns that require different context building approaches:
Root Config Form Managers use field_id values that match the dataclass type name (e.g., GlobalPipelineConfig, PipelineConfig). These represent the entire root configuration object, not a nested field within it. Widget names follow the pattern GlobalPipelineConfig.num_workers.
Nested Form Managers use field_id values that match actual dataclass field names (e.g., well_filter_config, zarr_config). These correspond directly to context fields with the same names. Widget names follow the pattern well_filter_config.well_filter.
The critical fixes eliminate artificial field naming:
Nested Form Managers: Removed
nested_prefix hack, now use actual field names directlyRoot Config Form Managers: Changed from artificial
configfield_id to dataclass type name
Root vs Nested Detection Pattern
The system uses generic detection logic to distinguish root configs from nested configs:
def _build_context_from_current_form_values(self, exclude_field=None):
current_context = get_current_global_config(GlobalPipelineConfig)
# Generic root vs nested detection
if hasattr(current_context, self.field_id):
# Nested config: field_id exists as actual field in context
# Examples: "well_filter_config", "zarr_config"
current_dataclass_instance = getattr(current_context, self.field_id)
else:
# Root config: field_id doesn't exist as field in context
# Examples: "GlobalPipelineConfig", "PipelineConfig"
current_dataclass_instance = current_context
This pattern works generically for any dataclass hierarchy without hardcoding specific class names.
Context Building Process
Normal Context Building Flow
_build_context_from_current_form_values() orchestrates context building by iterating through all form managers to collect current values. For each form manager, it reads widget values and combines them with parameter values, then builds context with all current form state for lazy dataclass resolution.
The method first checks hasattr(current_context, context_field_name) to verify the form’s dataclass exists in context. If found, it enters the widget reading loop where current widget values are collected and combined with existing parameter values.
Context Building During Reset
Reset operations call context building with an exclude_field parameter to exclude the target field from widget value reading. The exclusion logic in the widget reading loop checks exclude_field and param_name == exclude_field and skips reading the widget value for excluded fields.
This allows the context to be built without the field being reset, enabling lazy resolution to inherit from parent configurations instead of using the current (soon-to-be-reset) value.
The reset flow: user clicks reset → reset_parameter() calls context building with exclusion → context built without target field → lazy resolution inherits from parent → placeholder text updated with inherited value.
Debugging Patterns
Essential Debug Output
Add these debug prints to trace the inheritance system:
# In _build_context_from_current_form_values()
print(f"🔍 CONTEXT BUILD DEBUG: {self.field_id} building context with exclude_field='{exclude_field}'")
# Root vs nested detection debugging
if hasattr(current_context, self.field_id):
print(f"🔍 CONTEXT DEBUG: NESTED CONFIG - field_id='{self.field_id}' found in context")
else:
print(f"🔍 CONTEXT DEBUG: ROOT CONFIG - field_id='{self.field_id}' using current_context directly")
# In widget reading loop
print(f"🔍 EXCLUSION DEBUG: Checking param_name='{param_name}' vs exclude_field='{exclude_field}'")
if exclude_field and param_name == exclude_field:
print(f"🔍 WIDGET DEBUG: {self.field_id}.{param_name} EXCLUDED from context (reset)")
# In lazy resolution code
print(f"🔍 LAZY RESOLUTION DEBUG: Resolving {dataclass_name}.{field_name}")
print(f"🔍 LAZY RESOLUTION DEBUG: Context has {parent_field} = '{parent_value}'")
Common Debugging Scenarios
Root Config Context Building Failure:
# Symptom: Non-nested fields (num_workers, materialization_results_path) don't reset placeholders
# Cause: Using artificial field_id="config" instead of dataclass type name
# WRONG:
form_manager = ParameterFormManager.from_dataclass_instance(
field_id="config" # ❌ GlobalPipelineConfig has no "config" field
)
# CORRECT:
form_manager = ParameterFormManager.from_dataclass_instance(
field_id=type(current_config).__name__ # ✅ "GlobalPipelineConfig"
)
Nested Config Field Path Issues:
# Symptom: Nested fields don't update placeholders after sibling changes
# Cause: Using artificial "nested_" prefix instead of actual field names
# WRONG:
nested_manager = ParameterFormManager(..., field_id="nested_well_filter_config")
# CORRECT:
field_path = FieldPathDetector.find_field_path_for_type(parent_type, nested_type)
nested_manager = ParameterFormManager(..., field_id=field_path) # "well_filter_config"
Successful Operation Patterns
Successful Root Config Context Building:
🔍 CONTEXT DEBUG: ROOT CONFIG - field_id='GlobalPipelineConfig' using current_context directly
🔍 CONTEXT DEBUG: current_dataclass_instance=GlobalPipelineConfig(...)
Successful Nested Config Context Building:
🔍 CONTEXT DEBUG: NESTED CONFIG - field_id='well_filter_config' found in context
🔍 CONTEXT DEBUG: current_dataclass_instance=WellFilterConfig(...)
Successful Exclusion:
🔍 EXCLUSION DEBUG: Checking param_name='well_filter' vs exclude_field='well_filter'
🔍 WIDGET DEBUG: nested_step_materialization_config.well_filter EXCLUDED from context (reset)
Successful Inheritance:
🔍 LAZY RESOLUTION DEBUG: Resolving StepMaterializationConfig.well_filter
🔍 LAZY RESOLUTION DEBUG: Context has step_well_filter_config.well_filter = '789'
🔍 OVERRIDE CHECK: StepMaterializationConfig.well_filter default='None' has_override=False
🔍 OVERRIDE CHECK: StepWellFilterConfig.well_filter default='1' has_override=True
Common Failure Patterns
Context Building Failures
Field Naming Mismatch:
🔍 CONTEXT DEBUG: form_field_name='nested_well_filter_config', context_field_name='nested_well_filter_config', hasattr=False
This indicates the field naming fix is not applied. The context_field_name should strip the nested_ prefix to match the actual context field name.
Investigation Steps: 1. Verify the prefix stripping logic is applied 2. Check if field_id follows expected naming patterns 3. Confirm context contains the expected field names
Exclusion Logic Failures
Exclusion Not Working:
🔍 WIDGET DEBUG: nested_step_materialization_config.well_filter widget value = 'some_value'
If widget values are being read for the excluded field, exclusion is not working.
Investigation Steps: 1. Verify context building reaches the widget reading loop 2. Check if exclude_field parameter is passed correctly 3. Confirm param_name matches exclude_field exactly
Inheritance Resolution Failures
Wrong Inheritance Chain:
🔍 LAZY RESOLUTION DEBUG: Context has wrong_parent_field = 'unexpected_value'
This indicates context contains wrong parent values or inheritance logic errors.
Investigation Steps: 1. Trace lazy resolution debug output to verify inheritance path 2. Check if context building collected correct parent values 3. Verify inheritance decorators are properly configured
Testing and Validation
Automated Testing
Use tests.pyqt_gui.functional.test_reset_placeholder_simplified.TestResetPlaceholderInheritance.test_comprehensive_inheritance_chains() to validate inheritance chains. This test verifies multiple inheritance levels, reset functionality, and placeholder text accuracy through UI automation.
Manual Testing Protocol
Set Parent Field: Set parent field to concrete value (e.g.,
step_well_filter_config.well_filter = "789")Verify Child Inheritance: Verify child field shows inherited placeholder (e.g.,
step_materialization_config.well_filtershows “Pipeline default: 789”)Test Reset Functionality: Reset child field and verify placeholder updates correctly
Test Parent Reset: Reset parent field and verify child field updates to new inherited value
Validate Chain Propagation: Test multiple levels of inheritance to ensure chains propagate correctly
Known Issues and Limitations
Non-Nested Field Reset Bug
Non-nested fields don’t reset placeholder values properly when a config is saved and reopened. When reopened, resetting a concrete non-nested field causes the placeholder to show the concrete value instead of the inherited value.
This appears related to how the configuration cache system interacts with reset functionality for non-nested fields, where concrete values become part of cached context and reset logic may not properly exclude them.
Field Path Validation
Validation Checks:
def validate_field_path_mapping():
"""Ensure all form field_ids map correctly to context fields"""
from openhcs.core.config import GlobalPipelineConfig
import dataclasses
# Get all dataclass fields from GlobalPipelineConfig
context_fields = {f.name for f in dataclasses.fields(GlobalPipelineConfig)
if dataclasses.is_dataclass(f.type)}
# Verify form managers use these exact field names (no artificial prefixes)
assert "well_filter_config" in context_fields
assert "nested_well_filter_config" not in context_fields # Should not exist
return True
Root Config Validation:
def validate_root_config_field_id(form_manager, config_instance):
"""Ensure root config form managers use dataclass type name as field_id"""
expected_field_id = type(config_instance).__name__
actual_field_id = form_manager.field_id
assert actual_field_id == expected_field_id, f"Root config field_id should be '{expected_field_id}', got '{actual_field_id}'"
# Verify this field_id doesn't exist as a field in the config
assert not hasattr(config_instance, actual_field_id), f"field_id '{actual_field_id}' should not exist as field in {type(config_instance).__name__}"
Architecture Improvements Implemented
The field path system redesign has eliminated the fragile nested_ string prefix dependencies:
✅ Completed Improvements:
Eliminated artificial field naming: No more
nested_prefixes orconfigfield_idsDirect field path mapping: Form managers use actual dataclass field names
Root config detection: Generic
hasattr()logic works for any dataclass hierarchyContext building alignment: Field paths match dataclass structure exactly
Visual programming compliance: UI field names directly reflect code structure
This comprehensive debugging approach helps identify whether issues are in context building, exclusion logic, inheritance resolution, or field path mapping.