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

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 

12from objectstate.lazy_factory import has_lazy_resolution 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17# _has_concrete_field_override moved to dual_axis_resolver_recursive.py 

18# Placeholder service should not contain inheritance logic 

19 

20 

21class LazyDefaultPlaceholderService: 

22 """ 

23 Simplified placeholder service using new contextvars system. 

24  

25 Provides consistent placeholder pattern for lazy configuration classes 

26 using the same resolution mechanism as the compiler. 

27 """ 

28 

29 PLACEHOLDER_PREFIX = "Default" 

30 NONE_VALUE_TEXT = "(none)" 

31 

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. 

41 

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 

47 

48 Returns: 

49 Formatted placeholder text or None if no resolution possible 

50 """ 

51 prefix = placeholder_prefix or LazyDefaultPlaceholderService.PLACEHOLDER_PREFIX 

52 

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 ) 

58 

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) 

69 

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 

81 

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 

90 

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) 

105 

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}" 

115 

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 """ 

121 

122 class_name = dataclass_instance.__class__.__name__ 

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

124 

125 field_summaries = [] 

126 for field_name in all_fields: 

127 try: 

128 value = getattr(dataclass_instance, field_name) 

129 

130 # Skip None values to keep summary concise 

131 if value is None: 

132 continue 

133 

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) 

144 

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

146 

147 except (AttributeError, Exception): 

148 continue 

149 

150 if field_summaries: 

151 return ", ".join(field_summaries) 

152 else: 

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