Coverage for src/prosemark/templates/domain/values/placeholder_pattern.py: 100%

70 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-30 23:09 +0000

1"""PlaceholderPattern value object for placeholder syntax handling and validation.""" 

2 

3import re 

4from typing import Self 

5 

6from prosemark.templates.domain.exceptions.template_exceptions import InvalidPlaceholderError 

7 

8 

9class PlaceholderPattern: 

10 """Immutable value object representing a validated placeholder pattern. 

11 

12 This value object encapsulates a placeholder pattern like '{{variable_name}}' 

13 and provides parsing and validation capabilities. 

14 """ 

15 

16 # Regex for valid placeholder patterns: {{valid_identifier}} 

17 PLACEHOLDER_REGEX = re.compile(r'^\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}$') 

18 

19 def __init__(self, pattern: str) -> None: 

20 """Initialize a placeholder pattern with validation. 

21 

22 Args: 

23 pattern: The placeholder pattern string (e.g., "{{variable_name}}") 

24 

25 Raises: 

26 InvalidPlaceholderError: If the pattern has invalid syntax 

27 

28 """ 

29 if not isinstance(pattern, str): 

30 raise InvalidPlaceholderError('Placeholder pattern must be a string', placeholder_pattern=str(pattern)) 

31 

32 if not pattern.strip(): 

33 raise InvalidPlaceholderError('Placeholder pattern cannot be empty', placeholder_pattern=pattern) 

34 

35 match = self.PLACEHOLDER_REGEX.match(pattern) 

36 if not match: 

37 raise InvalidPlaceholderError( 

38 'Invalid placeholder pattern syntax. Must be {{valid_identifier}}', placeholder_pattern=pattern 

39 ) 

40 

41 self._pattern = pattern 

42 self._name = match.group(1) 

43 

44 @property 

45 def raw(self) -> str: 

46 """Get the raw placeholder pattern string.""" 

47 return self._pattern 

48 

49 @property 

50 def name(self) -> str: 

51 """Get the extracted variable name from the pattern.""" 

52 return self._name 

53 

54 @property 

55 def is_valid(self) -> bool: 

56 """Check if the pattern is valid (always True for constructed instances).""" 

57 return True 

58 

59 def matches_text(self, text: str) -> bool: 

60 """Check if this pattern appears in the given text. 

61 

62 Args: 

63 text: Text to search for this placeholder pattern 

64 

65 Returns: 

66 True if the pattern is found in the text 

67 

68 """ 

69 return self._pattern in text 

70 

71 def extract_from_text(self, text: str) -> list[str]: 

72 """Extract all occurrences of this pattern from text. 

73 

74 Args: 

75 text: Text to extract patterns from 

76 

77 Returns: 

78 List of pattern occurrences (may contain duplicates) 

79 

80 """ 

81 return re.findall(re.escape(self._pattern), text) 

82 

83 def replace_in_text(self, text: str, replacement: str) -> str: 

84 """Replace all occurrences of this pattern in text. 

85 

86 Args: 

87 text: Text to perform replacement in 

88 replacement: String to replace the pattern with 

89 

90 Returns: 

91 Text with all occurrences of the pattern replaced 

92 

93 """ 

94 return text.replace(self._pattern, replacement) 

95 

96 @classmethod 

97 def from_name(cls, variable_name: str) -> Self: 

98 """Create a placeholder pattern from a variable name. 

99 

100 Args: 

101 variable_name: The variable name (without braces) 

102 

103 Returns: 

104 New PlaceholderPattern instance 

105 

106 Raises: 

107 InvalidPlaceholderError: If the variable name is invalid 

108 

109 """ 

110 if not variable_name: 

111 raise InvalidPlaceholderError('Variable name cannot be empty') 

112 

113 # Validate that the name would be a valid Python identifier 

114 if ( 

115 not variable_name.replace('_', 'a') 

116 .replace('0', 'a') 

117 .replace('1', 'a') 

118 .replace('2', 'a') 

119 .replace('3', 'a') 

120 .replace('4', 'a') 

121 .replace('5', 'a') 

122 .replace('6', 'a') 

123 .replace('7', 'a') 

124 .replace('8', 'a') 

125 .replace('9', 'a') 

126 .isalpha() 

127 ): 

128 msg = f'Invalid variable name: {variable_name}. Must be a valid Python identifier' 

129 raise InvalidPlaceholderError(msg, placeholder_pattern=f'{ { {variable_name}} } ') 

130 

131 if variable_name[0].isdigit(): 

132 msg = f'Variable name cannot start with a digit: {variable_name}' 

133 raise InvalidPlaceholderError(msg, placeholder_pattern=f'{ { {variable_name}} } ') 

134 

135 pattern = f'{ { {variable_name}} } ' 

136 return cls(pattern) 

137 

138 @classmethod 

139 def extract_all_from_text(cls, text: str) -> list[Self]: 

140 """Extract all valid placeholder patterns from text. 

141 

142 Args: 

143 text: Text to extract placeholder patterns from 

144 

145 Returns: 

146 List of PlaceholderPattern instances found in text 

147 

148 Raises: 

149 InvalidPlaceholderError: If malformed patterns are found 

150 

151 """ 

152 # Find all potential placeholder patterns 

153 potential_patterns = re.findall(r'\{\{[^}]*\}\}', text) 

154 

155 patterns = [] 

156 for pattern in potential_patterns: 

157 try: 

158 patterns.append(cls(pattern)) 

159 except InvalidPlaceholderError as e: 

160 # Re-raise with more context about where the invalid pattern was found 

161 msg = f'Found malformed placeholder pattern in text: {pattern}' 

162 raise InvalidPlaceholderError(msg, placeholder_pattern=pattern) from e 

163 

164 return patterns 

165 

166 @classmethod 

167 def is_valid_pattern(cls, pattern: str) -> bool: 

168 """Check if a string is a valid placeholder pattern without raising exceptions. 

169 

170 Args: 

171 pattern: Pattern string to validate 

172 

173 Returns: 

174 True if the pattern is valid, False otherwise 

175 

176 """ 

177 try: 

178 cls(pattern) 

179 except InvalidPlaceholderError: 

180 return False 

181 else: 

182 return True 

183 

184 def __str__(self) -> str: 

185 """String representation of the placeholder pattern.""" 

186 return self._pattern 

187 

188 def __repr__(self) -> str: 

189 """Developer representation of the placeholder pattern.""" 

190 return f'PlaceholderPattern({self._pattern!r})' 

191 

192 def __eq__(self, other: object) -> bool: 

193 """Check equality with another PlaceholderPattern.""" 

194 if not isinstance(other, PlaceholderPattern): 

195 return NotImplemented 

196 return self._pattern == other._pattern 

197 

198 def __hash__(self) -> int: 

199 """Hash based on the pattern value.""" 

200 return hash(self._pattern)