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

103 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +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 

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 

158 class_name = dataclass_instance.__class__.__name__ 

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

160 

161 field_summaries = [] 

162 for field_name in all_fields: 

163 try: 

164 value = getattr(dataclass_instance, field_name) 

165 

166 # Skip None values to keep summary concise 

167 if value is None: 

168 continue 

169 

170 # Format different value types appropriately 

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

172 try: 

173 from openhcs.ui.shared.ui_utils import format_enum_display 

174 formatted_value = format_enum_display(value) 

175 except ImportError: 

176 formatted_value = str(value) 

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

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

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

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

181 else: 

182 formatted_value = str(value) 

183 

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

185 

186 except (AttributeError, Exception): 

187 continue 

188 

189 if field_summaries: 

190 return ", ".join(field_summaries) 

191 else: 

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

193 

194 

195# Backward compatibility functions 

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

197 """Backward compatibility wrapper.""" 

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