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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
1"""
2Generic lazy placeholder service for UI integration.
4Provides placeholder text resolution for lazy configuration dataclasses
5using contextvars-based context management.
6"""
8from typing import Any, Optional, Type
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 return LazyDefaultPlaceholderService._get_class_default_placeholder(
76 dataclass_type, field_name, prefix
77 )
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)
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
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
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
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
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)
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}"
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
159 class_name = dataclass_instance.__class__.__name__
160 all_fields = [f.name for f in dataclasses.fields(dataclass_instance)]
162 field_summaries = []
163 for field_name in all_fields:
164 try:
165 value = getattr(dataclass_instance, field_name)
167 # Skip None values to keep summary concise
168 if value is None:
169 continue
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)
185 field_summaries.append(f"{field_name}={formatted_value}")
187 except (AttributeError, Exception):
188 continue
190 if field_summaries:
191 return ", ".join(field_summaries)
192 else:
193 return f"{class_name} (default settings)"
196# Backward compatibility functions
197def get_lazy_resolved_placeholder(*args, **kwargs):
198 """Backward compatibility wrapper."""
199 return LazyDefaultPlaceholderService.get_lazy_resolved_placeholder(*args, **kwargs)