"""
Generic lazy placeholder service for UI integration.
Provides placeholder text resolution for lazy configuration dataclasses
using contextvars-based context management.
"""
from typing import Any, Optional
import dataclasses
import logging
logger = logging.getLogger(__name__)
# _has_concrete_field_override moved to dual_axis_resolver_recursive.py
# Placeholder service should not contain inheritance logic
[docs]
class LazyDefaultPlaceholderService:
"""
Simplified placeholder service using new contextvars system.
Provides consistent placeholder pattern for lazy configuration classes
using the same resolution mechanism as the compiler.
"""
PLACEHOLDER_PREFIX = "Default"
NONE_VALUE_TEXT = "(none)"
[docs]
@staticmethod
def has_lazy_resolution(dataclass_type: type) -> bool:
"""
Check if a type has lazy resolution capability.
Returns True for:
1. LazyDataclass types (all None defaults, used in PipelineConfig)
2. Concrete types with _has_lazy_resolution (used in GlobalPipelineConfig)
The distinction matters:
- is_lazy_dataclass() → only LazyDataclass types
- has_lazy_resolution() → any type that can resolve None via MRO
"""
from objectstate.lazy_factory import is_lazy_dataclass
# Check if it's a LazyDataclass OR has been bound with lazy resolution
return is_lazy_dataclass(dataclass_type) or getattr(dataclass_type, '_has_lazy_resolution', False)
[docs]
@staticmethod
def get_lazy_resolved_placeholder(
dataclass_type: type,
field_name: str,
placeholder_prefix: Optional[str] = None,
context_obj: Optional[Any] = None
) -> Optional[str]:
"""
Get placeholder text using the new contextvars system.
Args:
dataclass_type: The dataclass type to resolve for
field_name: Name of the field to resolve
placeholder_prefix: Optional prefix for placeholder text
context_obj: Optional context object (orchestrator, step, dataclass instance, etc.) - unused since context should be set externally
Returns:
Formatted placeholder text or None if no resolution possible
"""
prefix = placeholder_prefix or LazyDefaultPlaceholderService.PLACEHOLDER_PREFIX
# Check if type has lazy resolution (LazyDataclass OR concrete with _has_lazy_resolution)
if not LazyDefaultPlaceholderService.has_lazy_resolution(dataclass_type):
# Non-lazy type - use direct class default
return LazyDefaultPlaceholderService._get_class_default_placeholder(
dataclass_type, field_name, prefix
)
# Create instance and let lazy __getattribute__ handle context resolution
try:
instance = dataclass_type()
resolved_value = getattr(instance, field_name)
return LazyDefaultPlaceholderService._format_placeholder_text(resolved_value, prefix)
except Exception as e:
logger.debug(f"Failed to resolve {dataclass_type.__name__}.{field_name}: {e}")
# Fallback to class default
class_default = LazyDefaultPlaceholderService._get_class_default_value(dataclass_type, field_name)
return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix)
@staticmethod
def _get_class_default_placeholder(dataclass_type: type, field_name: str, prefix: str) -> Optional[str]:
"""Get placeholder for non-lazy dataclasses using class defaults."""
try:
# Use object.__getattribute__ to avoid triggering lazy __getattribute__ recursion
class_default = object.__getattribute__(dataclass_type, field_name)
if class_default is not None:
return LazyDefaultPlaceholderService._format_placeholder_text(class_default, prefix)
except AttributeError:
pass
return None
@staticmethod
def _get_class_default_value(dataclass_type: type, field_name: str) -> Any:
"""Get class default value for a field."""
try:
# Use object.__getattribute__ to avoid triggering lazy __getattribute__ recursion
return object.__getattribute__(dataclass_type, field_name)
except AttributeError:
return None
@staticmethod
def _format_placeholder_text(resolved_value: Any, prefix: str) -> Optional[str]:
"""Format resolved value into placeholder text."""
if resolved_value is None:
value_text = LazyDefaultPlaceholderService.NONE_VALUE_TEXT
elif hasattr(resolved_value, '__dataclass_fields__'):
value_text = LazyDefaultPlaceholderService._format_nested_dataclass_summary(resolved_value)
else:
# Apply proper formatting for different value types
if hasattr(resolved_value, 'value') and hasattr(resolved_value, 'name'): # Enum
# Format as "EnumName.VALUE" for display
value_text = f"{type(resolved_value).__name__}.{resolved_value.name}"
else:
value_text = str(resolved_value)
# Apply prefix formatting
if not prefix:
return value_text
elif prefix.endswith(': '):
return f"{prefix}{value_text}"
elif prefix.endswith(':'):
return f"{prefix} {value_text}"
else:
return f"{prefix}: {value_text}"
@staticmethod
def _format_nested_dataclass_summary(dataclass_instance) -> str:
"""
Format nested dataclass with all field values for user-friendly placeholders.
"""
class_name = dataclass_instance.__class__.__name__
all_fields = [f.name for f in dataclasses.fields(dataclass_instance)]
field_summaries = []
for field_name in all_fields:
try:
value = getattr(dataclass_instance, field_name)
# Skip None values to keep summary concise
if value is None:
continue
# Format different value types appropriately
if hasattr(value, 'value') and hasattr(value, 'name'): # Enum
# Format as "EnumName.VALUE" for display
formatted_value = f"{type(value).__name__}.{value.name}"
elif isinstance(value, str) and len(value) > 20: # Long strings
formatted_value = f"{value[:17]}..."
elif dataclasses.is_dataclass(value): # Nested dataclass
formatted_value = f"{value.__class__.__name__}(...)"
else:
formatted_value = str(value)
field_summaries.append(f"{field_name}={formatted_value}")
except (AttributeError, Exception):
continue
if field_summaries:
return ", ".join(field_summaries)
else:
return f"{class_name} (default settings)"
# Backward compatibility functions
[docs]
def get_lazy_resolved_placeholder(*args, **kwargs):
"""Backward compatibility wrapper."""
return LazyDefaultPlaceholderService.get_lazy_resolved_placeholder(*args, **kwargs)