Coverage for openhcs/core/lazy_placeholder_simplified.py: 15.4%

105 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +0000

1""" 

2Simplified lazy placeholder service using new contextvars system. 

3 

4Provides placeholder text resolution for lazy configuration dataclasses 

5using the new 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 result = LazyDefaultPlaceholderService._get_class_default_placeholder( 

76 dataclass_type, field_name, prefix 

77 ) 

78 return result 

79 

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

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

82 try: 

83 instance = dataclass_type() 

84 resolved_value = getattr(instance, field_name) 

85 result = LazyDefaultPlaceholderService._format_placeholder_text(resolved_value, prefix) 

86 except Exception as e: 

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

88 # Fallback to class default 

89 class_default = LazyDefaultPlaceholderService._get_class_default_value(dataclass_type, field_name) 

90 result = LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix) 

91 

92 return result 

93 

94 @staticmethod 

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

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

97 from openhcs.config_framework.lazy_factory import _lazy_type_registry 

98 

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

100 if registered_base_type == base_type: 

101 return lazy_type 

102 return None 

103 

104 

105 

106 @staticmethod 

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

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

109 try: 

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

111 class_default = object.__getattribute__(dataclass_type, field_name) 

112 if class_default is not None: 

113 return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix) 

114 except AttributeError: 

115 pass 

116 return None 

117 

118 @staticmethod 

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

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

121 try: 

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

123 return object.__getattribute__(dataclass_type, field_name) 

124 except AttributeError: 

125 return None 

126 

127 @staticmethod 

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

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

130 if resolved_value is None: 

131 value_text = LazyDefaultPlaceholderService.NONE_VALUE_TEXT 

132 elif hasattr(resolved_value, '__dataclass_fields__'): 

133 value_text = LazyDefaultPlaceholderService._format_nested_dataclass_summary(resolved_value) 

134 else: 

135 # Apply proper formatting for different value types 

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

137 try: 

138 from openhcs.ui.shared.ui_utils import format_enum_display 

139 value_text = format_enum_display(resolved_value) 

140 except ImportError: 

141 value_text = str(resolved_value) 

142 else: 

143 value_text = str(resolved_value) 

144 

145 # Apply prefix formatting 

146 if not prefix: 

147 return value_text 

148 elif prefix.endswith(': '): 

149 return f"{prefix}{value_text}" 

150 elif prefix.endswith(':'): 

151 return f"{prefix} {value_text}" 

152 else: 

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

154 

155 @staticmethod 

156 def _format_nested_dataclass_summary(dataclass_instance) -> str: 

157 """ 

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

159 """ 

160 

161 class_name = dataclass_instance.__class__.__name__ 

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

163 

164 field_summaries = [] 

165 for field_name in all_fields: 

166 try: 

167 value = getattr(dataclass_instance, field_name) 

168 

169 # Skip None values to keep summary concise 

170 if value is None: 

171 continue 

172 

173 # Format different value types appropriately 

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

175 try: 

176 from openhcs.ui.shared.ui_utils import format_enum_display 

177 formatted_value = format_enum_display(value) 

178 except ImportError: 

179 formatted_value = str(value) 

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

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

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

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

184 else: 

185 formatted_value = str(value) 

186 

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

188 

189 except (AttributeError, Exception): 

190 continue 

191 

192 if field_summaries: 

193 return ", ".join(field_summaries) 

194 else: 

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

196 

197 

198# Backward compatibility functions 

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

200 """Backward compatibility wrapper.""" 

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