Coverage for src/objectstate/placeholder.py: 24%
75 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 07:22 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 07:22 +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
9import dataclasses
10import logging
12from objectstate.lazy_factory import has_lazy_resolution
14logger = logging.getLogger(__name__)
17# _has_concrete_field_override moved to dual_axis_resolver_recursive.py
18# Placeholder service should not contain inheritance logic
21class LazyDefaultPlaceholderService:
22 """
23 Simplified placeholder service using new contextvars system.
25 Provides consistent placeholder pattern for lazy configuration classes
26 using the same resolution mechanism as the compiler.
27 """
29 PLACEHOLDER_PREFIX = "Default"
30 NONE_VALUE_TEXT = "(none)"
32 @staticmethod
33 def get_lazy_resolved_placeholder(
34 dataclass_type: type,
35 field_name: str,
36 placeholder_prefix: Optional[str] = None,
37 context_obj: Optional[Any] = None
38 ) -> Optional[str]:
39 """
40 Get placeholder text using the new contextvars system.
42 Args:
43 dataclass_type: The dataclass type to resolve for
44 field_name: Name of the field to resolve
45 placeholder_prefix: Optional prefix for placeholder text
46 context_obj: Optional context object (orchestrator, step, dataclass instance, etc.) - unused since context should be set externally
48 Returns:
49 Formatted placeholder text or None if no resolution possible
50 """
51 prefix = placeholder_prefix or LazyDefaultPlaceholderService.PLACEHOLDER_PREFIX
53 if not has_lazy_resolution(dataclass_type):
54 # Non-lazy type - use direct class default
55 return LazyDefaultPlaceholderService._get_class_default_placeholder(
56 dataclass_type, field_name, prefix
57 )
59 # Create instance and let lazy __getattribute__ handle context resolution
60 try:
61 instance = dataclass_type()
62 resolved_value = getattr(instance, field_name)
63 return LazyDefaultPlaceholderService._format_placeholder_text(resolved_value, prefix)
64 except Exception as e:
65 logger.debug(f"Failed to resolve {dataclass_type.__name__}.{field_name}: {e}")
66 # Fallback to class default
67 class_default = LazyDefaultPlaceholderService._get_class_default_value(dataclass_type, field_name)
68 return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix)
70 @staticmethod
71 def _get_class_default_placeholder(dataclass_type: type, field_name: str, prefix: str) -> Optional[str]:
72 """Get placeholder for non-lazy dataclasses using class defaults."""
73 try:
74 # Use object.__getattribute__ to avoid triggering lazy __getattribute__ recursion
75 class_default = object.__getattribute__(dataclass_type, field_name)
76 if class_default is not None:
77 return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix)
78 except AttributeError:
79 pass
80 return None
82 @staticmethod
83 def _get_class_default_value(dataclass_type: type, field_name: str) -> Any:
84 """Get class default value for a field."""
85 try:
86 # Use object.__getattribute__ to avoid triggering lazy __getattribute__ recursion
87 return object.__getattribute__(dataclass_type, field_name)
88 except AttributeError:
89 return None
91 @staticmethod
92 def _format_placeholder_text(resolved_value: Any, prefix: str) -> Optional[str]:
93 """Format resolved value into placeholder text."""
94 if resolved_value is None:
95 value_text = LazyDefaultPlaceholderService.NONE_VALUE_TEXT
96 elif hasattr(resolved_value, '__dataclass_fields__'):
97 value_text = LazyDefaultPlaceholderService._format_nested_dataclass_summary(resolved_value)
98 else:
99 # Apply proper formatting for different value types
100 if hasattr(resolved_value, 'value') and hasattr(resolved_value, 'name'): # Enum
101 # Format as "EnumName.VALUE" for display
102 value_text = f"{type(resolved_value).__name__}.{resolved_value.name}"
103 else:
104 value_text = str(resolved_value)
106 # Apply prefix formatting
107 if not prefix:
108 return value_text
109 elif prefix.endswith(': '):
110 return f"{prefix}{value_text}"
111 elif prefix.endswith(':'):
112 return f"{prefix} {value_text}"
113 else:
114 return f"{prefix}: {value_text}"
116 @staticmethod
117 def _format_nested_dataclass_summary(dataclass_instance) -> str:
118 """
119 Format nested dataclass with all field values for user-friendly placeholders.
120 """
122 class_name = dataclass_instance.__class__.__name__
123 all_fields = [f.name for f in dataclasses.fields(dataclass_instance)]
125 field_summaries = []
126 for field_name in all_fields:
127 try:
128 value = getattr(dataclass_instance, field_name)
130 # Skip None values to keep summary concise
131 if value is None:
132 continue
134 # Format different value types appropriately
135 if hasattr(value, 'value') and hasattr(value, 'name'): # Enum
136 # Format as "EnumName.VALUE" for display
137 formatted_value = f"{type(value).__name__}.{value.name}"
138 elif isinstance(value, str) and len(value) > 20: # Long strings
139 formatted_value = f"{value[:17]}..."
140 elif dataclasses.is_dataclass(value): # Nested dataclass
141 formatted_value = f"{value.__class__.__name__}(...)"
142 else:
143 formatted_value = str(value)
145 field_summaries.append(f"{field_name}={formatted_value}")
147 except (AttributeError, Exception):
148 continue
150 if field_summaries:
151 return ", ".join(field_summaries)
152 else:
153 return f"{class_name} (default settings)"