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
« 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.
4Provides placeholder text resolution for lazy configuration dataclasses
5using the new contextvars-based context management.
6"""
8from typing import Any, Optional
9import dataclasses
10import logging
12logger = logging.getLogger(__name__)
15# _has_concrete_field_override moved to dual_axis_resolver_recursive.py
16# Placeholder service should not contain inheritance logic
19class LazyDefaultPlaceholderService:
20 """
21 Simplified placeholder service using new contextvars system.
23 Provides consistent placeholder pattern for lazy configuration classes
24 using the same resolution mechanism as the compiler.
25 """
27 PLACEHOLDER_PREFIX = "Default"
28 NONE_VALUE_TEXT = "(none)"
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
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))
41 return (hasattr(dataclass_type, '_resolve_field_value') and
42 hasattr(dataclass_type, 'to_base_config'))
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.
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
60 Returns:
61 Formatted placeholder text or None if no resolution possible
62 """
63 prefix = placeholder_prefix or LazyDefaultPlaceholderService.PLACEHOLDER_PREFIX
65 # Check if this is a lazy dataclass
66 is_lazy = LazyDefaultPlaceholderService.has_lazy_resolution(dataclass_type)
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
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)
92 return result
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
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
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
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
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)
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}"
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 """
161 class_name = dataclass_instance.__class__.__name__
162 all_fields = [f.name for f in dataclasses.fields(dataclass_instance)]
164 field_summaries = []
165 for field_name in all_fields:
166 try:
167 value = getattr(dataclass_instance, field_name)
169 # Skip None values to keep summary concise
170 if value is None:
171 continue
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)
187 field_summaries.append(f"{field_name}={formatted_value}")
189 except (AttributeError, Exception):
190 continue
192 if field_summaries:
193 return ", ".join(field_summaries)
194 else:
195 return f"{class_name} (default settings)"
198# Backward compatibility functions
199def get_lazy_resolved_placeholder(*args, **kwargs):
200 """Backward compatibility wrapper."""
201 return LazyDefaultPlaceholderService.get_lazy_resolved_placeholder(*args, **kwargs)