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

1"""Unified parameter analysis interface for all parameter sources in OpenHCS TUI. 

2 

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 

8 

9Replaces the fragmented approach of SignatureAnalyzer vs FieldIntrospector. 

10""" 

11 

12import inspect 

13import dataclasses 

14from typing import Dict, Union, Callable, Type, Any, Optional 

15from dataclasses import dataclass 

16 

17from .signature_analyzer import SignatureAnalyzer, ParameterInfo, DocstringExtractor 

18 

19 

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" 

29 

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 ) 

41 

42 

43class UnifiedParameterAnalyzer: 

44 """Single interface for analyzing parameters from any source. 

45  

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 """ 

50 

51 @staticmethod 

52 def analyze(target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]: 

53 """Analyze parameters from any source. 

54  

55 Args: 

56 target: Function, method, dataclass type, or instance to analyze 

57  

58 Returns: 

59 Dictionary mapping parameter names to UnifiedParameterInfo objects 

60  

61 Examples: 

62 # Function analysis 

63 param_info = UnifiedParameterAnalyzer.analyze(my_function) 

64  

65 # Dataclass analysis 

66 param_info = UnifiedParameterAnalyzer.analyze(MyDataclass) 

67  

68 # Instance analysis 

69 param_info = UnifiedParameterAnalyzer.analyze(my_instance) 

70 """ 

71 if target is None: 

72 return {} 

73 

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) 

93 

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) 

99 

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 ) 

107 

108 return unified_params 

109 

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) 

116 

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 ) 

124 

125 return unified_params 

126 

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 = {} 

133 

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 

138 

139 # Skip classes without custom __init__ 

140 if not hasattr(cls, '__init__') or cls.__init__ == object.__init__: 

141 continue 

142 

143 try: 

144 # Analyze this class's constructor 

145 class_params = UnifiedParameterAnalyzer._analyze_callable(cls.__init__) 

146 

147 # Remove 'self' parameter 

148 if 'self' in class_params: 

149 del class_params['self'] 

150 

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 

156 

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) 

162 

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 ) 

172 

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 

177 

178 return all_params 

179 

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) 

186 

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 

192 

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) 

203 

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 ) 

213 

214 return unified_params 

215 

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. 

219  

220 This method provides enhanced analysis that can handle nested dataclasses 

221 and maintain parent context information. 

222  

223 Args: 

224 target: The target to analyze 

225 parent_info: Optional parent parameter information for context 

226  

227 Returns: 

228 Dictionary of unified parameter information with nested support 

229 """ 

230 base_params = UnifiedParameterAnalyzer.analyze(target) 

231 

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 

236 

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 ) 

248 

249 return enhanced_params 

250 

251 

252# Backward compatibility aliases 

253# These allow existing code to continue working while migration happens 

254ParameterAnalyzer = UnifiedParameterAnalyzer 

255analyze_parameters = UnifiedParameterAnalyzer.analyze