Coverage for openhcs/config_framework/placeholder.py: 15.5%

104 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +0000

1""" 

2Generic lazy placeholder service for UI integration. 

3 

4Provides placeholder text resolution for lazy configuration dataclasses 

5using contextvars-based context management. 

6""" 

7 

8from typing import Any, Optional, Type 

9import dataclasses 

10import logging 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15# _has_concrete_field_override moved to dual_axis_resolver_recursive.py 

16# Placeholder service should not contain inheritance logic 

17 

18 

19class LazyDefaultPlaceholderService: 

20 """ 

21 Simplified placeholder service using new contextvars system. 

22  

23 Provides consistent placeholder pattern for lazy configuration classes 

24 using the same resolution mechanism as the compiler. 

25 """ 

26 

27 PLACEHOLDER_PREFIX = "Default" 

28 NONE_VALUE_TEXT = "(none)" 

29 

30 @staticmethod 

31 def has_lazy_resolution(dataclass_type: type) -> bool: 

32 """Check if dataclass has lazy resolution methods (created by factory).""" 

33 from typing import get_origin, get_args, Union 

34 

35 # Unwrap Optional types (Union[Type, None]) 

36 if get_origin(dataclass_type) is Union: 

37 args = get_args(dataclass_type) 

38 if len(args) == 2 and type(None) in args: 

39 dataclass_type = next(arg for arg in args if arg is not type(None)) 

40 

41 return (hasattr(dataclass_type, '_resolve_field_value') and 

42 hasattr(dataclass_type, 'to_base_config')) 

43 

44 @staticmethod 

45 def get_lazy_resolved_placeholder( 

46 dataclass_type: type, 

47 field_name: str, 

48 placeholder_prefix: Optional[str] = None, 

49 context_obj: Optional[Any] = None 

50 ) -> Optional[str]: 

51 """ 

52 Get placeholder text using the new contextvars system. 

53 

54 Args: 

55 dataclass_type: The dataclass type to resolve for 

56 field_name: Name of the field to resolve 

57 placeholder_prefix: Optional prefix for placeholder text 

58 context_obj: Optional context object (orchestrator, step, dataclass instance, etc.) - unused since context should be set externally 

59 

60 Returns: 

61 Formatted placeholder text or None if no resolution possible 

62 """ 

63 prefix = placeholder_prefix or LazyDefaultPlaceholderService.PLACEHOLDER_PREFIX 

64 

65 # Check if this is a lazy dataclass 

66 is_lazy = LazyDefaultPlaceholderService.has_lazy_resolution(dataclass_type) 

67 

68 # If not lazy, try to find the lazy version 

69 if not is_lazy: 

70 lazy_type = LazyDefaultPlaceholderService._get_lazy_type_for_base(dataclass_type) 

71 if lazy_type: 

72 dataclass_type = lazy_type 

73 else: 

74 # Use direct class default for non-lazy types 

75 return LazyDefaultPlaceholderService._get_class_default_placeholder( 

76 dataclass_type, field_name, prefix 

77 ) 

78 

79 # Simple approach: Create new instance and let lazy system handle context resolution 

80 # The context_obj parameter is unused since context should be set externally via config_context() 

81 try: 

82 instance = dataclass_type() 

83 resolved_value = getattr(instance, field_name) 

84 return LazyDefaultPlaceholderService._format_placeholder_text(resolved_value, prefix) 

85 except Exception as e: 

86 logger.debug(f"Failed to resolve {dataclass_type.__name__}.{field_name}: {e}") 

87 # Fallback to class default 

88 class_default = LazyDefaultPlaceholderService._get_class_default_value(dataclass_type, field_name) 

89 return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix) 

90 

91 @staticmethod 

92 def _get_lazy_type_for_base(base_type: type) -> Optional[type]: 

93 """Get the lazy type for a base dataclass type (reverse lookup).""" 

94 from openhcs.config_framework.lazy_factory import _lazy_type_registry 

95 

96 for lazy_type, registered_base_type in _lazy_type_registry.items(): 

97 if registered_base_type == base_type: 

98 return lazy_type 

99 return None 

100 

101 

102 

103 @staticmethod 

104 def _get_class_default_placeholder(dataclass_type: type, field_name: str, prefix: str) -> Optional[str]: 

105 """Get placeholder for non-lazy dataclasses using class defaults.""" 

106 try: 

107 # Use object.__getattribute__ to avoid triggering lazy __getattribute__ recursion 

108 class_default = object.__getattribute__(dataclass_type, field_name) 

109 if class_default is not None: 

110 return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix) 

111 except AttributeError: 

112 pass 

113 return None 

114 

115 @staticmethod 

116 def _get_class_default_value(dataclass_type: type, field_name: str) -> Any: 

117 """Get class default value for a field.""" 

118 try: 

119 # Use object.__getattribute__ to avoid triggering lazy __getattribute__ recursion 

120 return object.__getattribute__(dataclass_type, field_name) 

121 except AttributeError: 

122 return None 

123 

124 @staticmethod 

125 def _format_placeholder_text(resolved_value: Any, prefix: str) -> Optional[str]: 

126 """Format resolved value into placeholder text.""" 

127 if resolved_value is None: 

128 value_text = LazyDefaultPlaceholderService.NONE_VALUE_TEXT 

129 elif hasattr(resolved_value, '__dataclass_fields__'): 

130 value_text = LazyDefaultPlaceholderService._format_nested_dataclass_summary(resolved_value) 

131 else: 

132 # Apply proper formatting for different value types 

133 if hasattr(resolved_value, 'value') and hasattr(resolved_value, 'name'): # Enum 

134 try: 

135 from openhcs.ui.shared.ui_utils import format_enum_display 

136 value_text = format_enum_display(resolved_value) 

137 except ImportError: 

138 value_text = str(resolved_value) 

139 else: 

140 value_text = str(resolved_value) 

141 

142 # Apply prefix formatting 

143 if not prefix: 

144 return value_text 

145 elif prefix.endswith(': '): 

146 return f"{prefix}{value_text}" 

147 elif prefix.endswith(':'): 

148 return f"{prefix} {value_text}" 

149 else: 

150 return f"{prefix}: {value_text}" 

151 

152 @staticmethod 

153 def _format_nested_dataclass_summary(dataclass_instance) -> str: 

154 """ 

155 Format nested dataclass with all field values for user-friendly placeholders. 

156 """ 

157 import dataclasses 

158 

159 class_name = dataclass_instance.__class__.__name__ 

160 all_fields = [f.name for f in dataclasses.fields(dataclass_instance)] 

161 

162 field_summaries = [] 

163 for field_name in all_fields: 

164 try: 

165 value = getattr(dataclass_instance, field_name) 

166 

167 # Skip None values to keep summary concise 

168 if value is None: 

169 continue 

170 

171 # Format different value types appropriately 

172 if hasattr(value, 'value') and hasattr(value, 'name'): # Enum 

173 try: 

174 from openhcs.ui.shared.ui_utils import format_enum_display 

175 formatted_value = format_enum_display(value) 

176 except ImportError: 

177 formatted_value = str(value) 

178 elif isinstance(value, str) and len(value) > 20: # Long strings 

179 formatted_value = f"{value[:17]}..." 

180 elif dataclasses.is_dataclass(value): # Nested dataclass 

181 formatted_value = f"{value.__class__.__name__}(...)" 

182 else: 

183 formatted_value = str(value) 

184 

185 field_summaries.append(f"{field_name}={formatted_value}") 

186 

187 except (AttributeError, Exception): 

188 continue 

189 

190 if field_summaries: 

191 return ", ".join(field_summaries) 

192 else: 

193 return f"{class_name} (default settings)" 

194 

195 

196# Backward compatibility functions 

197def get_lazy_resolved_placeholder(*args, **kwargs): 

198 """Backward compatibility wrapper.""" 

199 return LazyDefaultPlaceholderService.get_lazy_resolved_placeholder(*args, **kwargs)