Coverage for src/prosemark/domain/placeholder_summary.py: 100%
44 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""PlaceholderSummary value object for discovered placeholder information."""
3from dataclasses import dataclass
6@dataclass(frozen=True)
7class PlaceholderSummary:
8 """Represents discovered placeholder information before materialization.
10 This value object captures the essential information about a placeholder
11 discovered in the binder, used for planning and executing batch materialization.
13 Args:
14 display_title: Title of the placeholder
15 position: Position in binder hierarchy (e.g., "[0][1]")
16 parent_title: Parent item title if applicable
17 depth: Nesting level in binder hierarchy
19 Raises:
20 ValueError: If validation rules are violated during construction
22 """
24 display_title: str
25 position: str
26 parent_title: str | None
27 depth: int
29 def __post_init__(self) -> None:
30 """Validate the placeholder summary after construction."""
31 # Validate display_title is non-empty
32 if not self.display_title or not self.display_title.strip():
33 msg = 'Display title must be non-empty string'
34 raise ValueError(msg)
36 # Validate position follows hierarchical format
37 if not self.position or not self._is_valid_position(self.position):
38 msg = f"Position must follow hierarchical format, got '{self.position}'"
39 raise ValueError(msg)
41 # Validate depth is non-negative
42 if self.depth < 0:
43 msg = f'Depth must be non-negative integer, got {self.depth}'
44 raise ValueError(msg)
46 # Validate parent_title consistency with depth
47 if self.depth == 0 and self.parent_title is not None:
48 msg = f"Root level items (depth=0) cannot have parent_title, got '{self.parent_title}'"
49 raise ValueError(msg)
51 # Note: We don't enforce that depth > 0 requires parent_title because
52 # parent information might not always be available during discovery
54 @staticmethod
55 def _is_valid_position(position: str) -> bool:
56 """Check if position follows the expected hierarchical format."""
57 # Pattern: [0][1][2]... (one or more bracketed integers)
58 import re
60 pattern = r'^(\[[0-9]+\])+$'
61 return bool(re.match(pattern, position))
63 @property
64 def is_root_level(self) -> bool:
65 """Check if this is a root-level placeholder."""
66 return self.depth == 0
68 @property
69 def hierarchy_path(self) -> str:
70 """Generate a human-readable hierarchy path."""
71 if self.parent_title:
72 return f'{self.parent_title} > {self.display_title}'
73 return self.display_title
75 @property
76 def position_indices(self) -> list[int]:
77 """Extract position indices from the position string."""
78 import re
80 # Extract numbers from [n][m] format
81 indices = re.findall(r'\[(\d+)\]', self.position)
82 return [int(idx) for idx in indices]
84 def with_updated_position(self, new_position: str) -> 'PlaceholderSummary':
85 """Create a new instance with updated position."""
86 return PlaceholderSummary(
87 display_title=self.display_title, position=new_position, parent_title=self.parent_title, depth=self.depth
88 )
90 def __str__(self) -> str:
91 """Generate human-readable string representation."""
92 if self.parent_title:
93 return f"'{self.display_title}' (under '{self.parent_title}', depth={self.depth})"
94 return f"'{self.display_title}' (depth={self.depth})"