Coverage for openhcs/textual_tui/widgets/shared/unified_parameter_analyzer.py: 0.0%
92 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
1"""Unified parameter analysis interface for all parameter sources in OpenHCS TUI.
3This module provides a single, consistent interface for analyzing parameters from:
4- Functions and methods
5- Dataclasses and their fields
6- Nested dataclass structures
7- Any callable or type with parameters
9Replaces the fragmented approach of SignatureAnalyzer vs FieldIntrospector.
10"""
12import inspect
13import dataclasses
14from typing import Dict, Union, Callable, Type, Any, Optional
15from dataclasses import dataclass
17from .signature_analyzer import SignatureAnalyzer, ParameterInfo, DocstringExtractor
20@dataclass
21class UnifiedParameterInfo:
22 """Unified parameter information that works for all parameter sources."""
23 name: str
24 param_type: Type
25 default_value: Any
26 is_required: bool
27 description: Optional[str] = None
28 source_type: str = "unknown" # "function", "dataclass", "nested"
30 @classmethod
31 def from_parameter_info(cls, param_info: ParameterInfo, source_type: str = "function") -> "UnifiedParameterInfo":
32 """Convert from existing ParameterInfo to unified format."""
33 return cls(
34 name=param_info.name,
35 param_type=param_info.param_type,
36 default_value=param_info.default_value,
37 is_required=param_info.is_required,
38 description=param_info.description,
39 source_type=source_type
40 )
43class UnifiedParameterAnalyzer:
44 """Single interface for analyzing parameters from any source.
46 This class provides a unified way to extract parameter information
47 from functions, dataclasses, and other parameter sources, ensuring
48 consistent behavior across the entire application.
49 """
51 @staticmethod
52 def analyze(target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
53 """Analyze parameters from any source.
55 Args:
56 target: Function, method, dataclass type, or instance to analyze
58 Returns:
59 Dictionary mapping parameter names to UnifiedParameterInfo objects
61 Examples:
62 # Function analysis
63 param_info = UnifiedParameterAnalyzer.analyze(my_function)
65 # Dataclass analysis
66 param_info = UnifiedParameterAnalyzer.analyze(MyDataclass)
68 # Instance analysis
69 param_info = UnifiedParameterAnalyzer.analyze(my_instance)
70 """
71 if target is None:
72 return {}
74 # Determine the type of target and route to appropriate analyzer
75 if inspect.isfunction(target) or inspect.ismethod(target):
76 return UnifiedParameterAnalyzer._analyze_callable(target)
77 elif inspect.isclass(target):
78 if dataclasses.is_dataclass(target):
79 return UnifiedParameterAnalyzer._analyze_dataclass_type(target)
80 else:
81 # Try to analyze constructor
82 return UnifiedParameterAnalyzer._analyze_callable(target.__init__)
83 elif dataclasses.is_dataclass(target):
84 # Instance of dataclass
85 return UnifiedParameterAnalyzer._analyze_dataclass_instance(target)
86 else:
87 # Try to analyze as callable
88 if callable(target):
89 return UnifiedParameterAnalyzer._analyze_callable(target)
90 else:
91 # For regular object instances (like step instances), analyze their class constructor
92 return UnifiedParameterAnalyzer._analyze_object_instance(target)
94 @staticmethod
95 def _analyze_callable(callable_obj: Callable) -> Dict[str, UnifiedParameterInfo]:
96 """Analyze a callable (function, method, etc.)."""
97 # Use existing SignatureAnalyzer for callables
98 param_info_dict = SignatureAnalyzer.analyze(callable_obj)
100 # Convert to unified format
101 unified_params = {}
102 for name, param_info in param_info_dict.items():
103 unified_params[name] = UnifiedParameterInfo.from_parameter_info(
104 param_info,
105 source_type="function"
106 )
108 return unified_params
110 @staticmethod
111 def _analyze_dataclass_type(dataclass_type: Type) -> Dict[str, UnifiedParameterInfo]:
112 """Analyze a dataclass type using existing SignatureAnalyzer infrastructure."""
113 # CRITICAL FIX: Use existing SignatureAnalyzer._analyze_dataclass method
114 # which already handles all the docstring extraction properly
115 param_info_dict = SignatureAnalyzer._analyze_dataclass(dataclass_type)
117 # Convert to unified format
118 unified_params = {}
119 for name, param_info in param_info_dict.items():
120 unified_params[name] = UnifiedParameterInfo.from_parameter_info(
121 param_info,
122 source_type="dataclass"
123 )
125 return unified_params
127 @staticmethod
128 def _analyze_object_instance(instance: object) -> Dict[str, UnifiedParameterInfo]:
129 """Analyze a regular object instance by examining its full inheritance hierarchy."""
130 # Use MRO to get all constructor parameters from the inheritance chain
131 instance_class = type(instance)
132 all_params = {}
134 # Traverse MRO from most specific to most general (like dual-axis resolver)
135 for cls in instance_class.__mro__:
136 if cls == object:
137 continue
139 # Skip classes without custom __init__
140 if not hasattr(cls, '__init__') or cls.__init__ == object.__init__:
141 continue
143 try:
144 # Analyze this class's constructor
145 class_params = UnifiedParameterAnalyzer._analyze_callable(cls.__init__)
147 # Remove 'self' parameter
148 if 'self' in class_params:
149 del class_params['self']
151 # Special handling for **kwargs - if we see 'kwargs', skip this class
152 # and let parent classes provide the actual parameters
153 if 'kwargs' in class_params and len(class_params) <= 2:
154 # This class uses **kwargs, skip it and let parent classes define parameters
155 continue
157 # Add parameters that haven't been seen yet (most specific wins)
158 for param_name, param_info in class_params.items():
159 if param_name not in all_params and param_name != 'kwargs':
160 # Get current value from instance if it exists
161 current_value = getattr(instance, param_name, param_info.default_value)
163 # Create parameter info with current value
164 all_params[param_name] = UnifiedParameterInfo(
165 name=param_name,
166 param_type=param_info.param_type,
167 default_value=current_value,
168 is_required=param_info.is_required,
169 description=param_info.description, # CRITICAL FIX: Include description
170 source_type="object_instance"
171 )
173 except Exception:
174 # Skip classes that can't be analyzed - this is legitimate since some classes
175 # in MRO might not have analyzable constructors (e.g., ABC, object)
176 continue
178 return all_params
180 @staticmethod
181 def _analyze_dataclass_instance(instance: object) -> Dict[str, UnifiedParameterInfo]:
182 """Analyze a dataclass instance."""
183 # Get the type and analyze it
184 dataclass_type = type(instance)
185 unified_params = UnifiedParameterAnalyzer._analyze_dataclass_type(dataclass_type)
187 # Check if this specific instance is a lazy config - if so, use raw field values
188 from openhcs.config_framework.lazy_factory import get_base_type_for_lazy
189 # CRITICAL FIX: Don't check class name - PipelineConfig is lazy but doesn't start with "Lazy"
190 # get_base_type_for_lazy() is the authoritative check for lazy dataclasses
191 is_lazy_config = get_base_type_for_lazy(dataclass_type) is not None
193 # Update default values with current instance values
194 for name, param_info in unified_params.items():
195 if hasattr(instance, name):
196 if is_lazy_config:
197 # For lazy configs, get raw field value to avoid triggering resolution
198 # Use object.__getattribute__() to bypass lazy property getters
199 current_value = object.__getattribute__(instance, name)
200 else:
201 # For regular dataclasses, use normal getattr
202 current_value = getattr(instance, name)
204 # Create new UnifiedParameterInfo with current value as default
205 unified_params[name] = UnifiedParameterInfo(
206 name=param_info.name,
207 param_type=param_info.param_type,
208 default_value=current_value,
209 is_required=param_info.is_required,
210 description=param_info.description,
211 source_type="dataclass_instance"
212 )
214 return unified_params
216 @staticmethod
217 def analyze_nested(target: Union[Callable, Type, object], parent_info: Dict[str, UnifiedParameterInfo] = None) -> Dict[str, UnifiedParameterInfo]:
218 """Analyze parameters with nested dataclass support.
220 This method provides enhanced analysis that can handle nested dataclasses
221 and maintain parent context information.
223 Args:
224 target: The target to analyze
225 parent_info: Optional parent parameter information for context
227 Returns:
228 Dictionary of unified parameter information with nested support
229 """
230 base_params = UnifiedParameterAnalyzer.analyze(target)
232 # For each parameter, check if it's a nested dataclass
233 enhanced_params = {}
234 for name, param_info in base_params.items():
235 enhanced_params[name] = param_info
237 # If this parameter is a dataclass, mark it as having nested structure
238 if dataclasses.is_dataclass(param_info.param_type):
239 # Update source type to indicate nesting capability
240 enhanced_params[name] = UnifiedParameterInfo(
241 name=param_info.name,
242 param_type=param_info.param_type,
243 default_value=param_info.default_value,
244 is_required=param_info.is_required,
245 description=param_info.description,
246 source_type=f"{param_info.source_type}_nested"
247 )
249 return enhanced_params
252# Backward compatibility aliases
253# These allow existing code to continue working while migration happens
254ParameterAnalyzer = UnifiedParameterAnalyzer
255analyze_parameters = UnifiedParameterAnalyzer.analyze