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

1"""PlaceholderSummary value object for discovered placeholder information.""" 

2 

3from dataclasses import dataclass 

4 

5 

6@dataclass(frozen=True) 

7class PlaceholderSummary: 

8 """Represents discovered placeholder information before materialization. 

9 

10 This value object captures the essential information about a placeholder 

11 discovered in the binder, used for planning and executing batch materialization. 

12 

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 

18 

19 Raises: 

20 ValueError: If validation rules are violated during construction 

21 

22 """ 

23 

24 display_title: str 

25 position: str 

26 parent_title: str | None 

27 depth: int 

28 

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) 

35 

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) 

40 

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) 

45 

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) 

50 

51 # Note: We don't enforce that depth > 0 requires parent_title because 

52 # parent information might not always be available during discovery 

53 

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 

59 

60 pattern = r'^(\[[0-9]+\])+$' 

61 return bool(re.match(pattern, position)) 

62 

63 @property 

64 def is_root_level(self) -> bool: 

65 """Check if this is a root-level placeholder.""" 

66 return self.depth == 0 

67 

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 

74 

75 @property 

76 def position_indices(self) -> list[int]: 

77 """Extract position indices from the position string.""" 

78 import re 

79 

80 # Extract numbers from [n][m] format 

81 indices = re.findall(r'\[(\d+)\]', self.position) 

82 return [int(idx) for idx in indices] 

83 

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 ) 

89 

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