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

56 statements  

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

1"""TemplatePath value object for template path handling and validation.""" 

2 

3from pathlib import Path 

4from typing import Self 

5 

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

7 TemplateNotFoundError, 

8 TemplateValidationError, 

9) 

10 

11 

12class TemplatePath: 

13 """Immutable value object representing a validated template file path. 

14 

15 This value object encapsulates a file system path that points to a template file, 

16 ensuring the path exists and meets basic template file requirements. 

17 """ 

18 

19 def __init__(self, path: Path | str) -> None: 

20 """Initialize a template path with validation. 

21 

22 Args: 

23 path: File system path to a template file 

24 

25 Raises: 

26 TemplateNotFoundError: If the path does not exist 

27 TemplateValidationError: If the path is not a valid template file 

28 

29 """ 

30 if isinstance(path, str): 

31 path = Path(path) 

32 

33 if not path.exists(): 

34 raise TemplateNotFoundError( 

35 template_name=path.name, search_path=str(path.parent) if path.parent != path else None 

36 ) 

37 

38 if not path.is_file(): 

39 msg = f'Template path must point to a file, not a directory: {path}' 

40 raise TemplateValidationError(msg, template_path=str(path)) 

41 

42 if path.suffix != '.md': 

43 msg = f'Template files must have .md extension: {path}' 

44 raise TemplateValidationError(msg, template_path=str(path)) 

45 

46 self._path = path.resolve() 

47 

48 @property 

49 def value(self) -> Path: 

50 """Get the absolute path to the template file.""" 

51 return self._path 

52 

53 @property 

54 def name(self) -> str: 

55 """Get the template name (filename without .md extension).""" 

56 return self._path.stem 

57 

58 @property 

59 def exists(self) -> bool: 

60 """Check if the template file still exists on disk.""" 

61 return self._path.exists() 

62 

63 @property 

64 def is_readable(self) -> bool: 

65 """Check if the template file is readable.""" 

66 try: 

67 return bool(self._path.is_file() and self._path.stat().st_mode & 0o400) 

68 except (OSError, PermissionError): # pragma: no cover 

69 return False 

70 

71 @property 

72 def parent_directory(self) -> Path: 

73 """Get the parent directory of the template file.""" 

74 return self._path.parent 

75 

76 def read_content(self) -> str: 

77 """Read the complete content of the template file. 

78 

79 Returns: 

80 The full text content of the template file 

81 

82 Raises: 

83 TemplateNotFoundError: If the file no longer exists 

84 PermissionError: If the file is not readable 

85 

86 """ 

87 if not self.exists: 

88 raise TemplateNotFoundError(template_name=self.name, search_path=str(self.parent_directory)) 

89 

90 try: 

91 return self._path.read_text(encoding='utf-8') 

92 except PermissionError as e: # pragma: no cover 

93 msg = f'Cannot read template file: {self._path}' 

94 raise PermissionError(msg) from e 

95 

96 def get_relative_path(self, base_directory: Path) -> Path: 

97 """Get the path relative to a base directory. 

98 

99 Args: 

100 base_directory: The base directory to calculate relative path from 

101 

102 Returns: 

103 Path relative to the base directory 

104 

105 """ 

106 try: 

107 return self._path.relative_to(base_directory) 

108 except ValueError: 

109 # Path is not relative to base_directory 

110 return self._path 

111 

112 @classmethod 

113 def from_name_and_directory(cls, name: str, directory: Path) -> Self: 

114 """Create a TemplatePath from template name and directory. 

115 

116 Args: 

117 name: Template name (without .md extension) 

118 directory: Directory containing the template 

119 

120 Returns: 

121 New TemplatePath instance 

122 

123 Raises: 

124 TemplateNotFoundError: If the template file does not exist 

125 TemplateValidationError: If the path is invalid 

126 

127 """ 

128 template_file = directory / f'{name}.md' if not name.endswith('.md') else directory / name 

129 

130 return cls(template_file) 

131 

132 def __str__(self) -> str: 

133 """String representation of the template path.""" 

134 return str(self._path) 

135 

136 def __repr__(self) -> str: 

137 """Developer representation of the template path.""" 

138 return f'TemplatePath({self._path!r})' 

139 

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

141 """Check equality with another TemplatePath.""" 

142 if not isinstance(other, TemplatePath): 

143 return NotImplemented 

144 return self._path == other._path 

145 

146 def __hash__(self) -> int: 

147 """Hash based on the path value.""" 

148 return hash(self._path)