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
« 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."""
3from pathlib import Path
4from typing import Self
6from prosemark.templates.domain.exceptions.template_exceptions import (
7 TemplateNotFoundError,
8 TemplateValidationError,
9)
12class TemplatePath:
13 """Immutable value object representing a validated template file path.
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 """
19 def __init__(self, path: Path | str) -> None:
20 """Initialize a template path with validation.
22 Args:
23 path: File system path to a template file
25 Raises:
26 TemplateNotFoundError: If the path does not exist
27 TemplateValidationError: If the path is not a valid template file
29 """
30 if isinstance(path, str):
31 path = Path(path)
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 )
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))
42 if path.suffix != '.md':
43 msg = f'Template files must have .md extension: {path}'
44 raise TemplateValidationError(msg, template_path=str(path))
46 self._path = path.resolve()
48 @property
49 def value(self) -> Path:
50 """Get the absolute path to the template file."""
51 return self._path
53 @property
54 def name(self) -> str:
55 """Get the template name (filename without .md extension)."""
56 return self._path.stem
58 @property
59 def exists(self) -> bool:
60 """Check if the template file still exists on disk."""
61 return self._path.exists()
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
71 @property
72 def parent_directory(self) -> Path:
73 """Get the parent directory of the template file."""
74 return self._path.parent
76 def read_content(self) -> str:
77 """Read the complete content of the template file.
79 Returns:
80 The full text content of the template file
82 Raises:
83 TemplateNotFoundError: If the file no longer exists
84 PermissionError: If the file is not readable
86 """
87 if not self.exists:
88 raise TemplateNotFoundError(template_name=self.name, search_path=str(self.parent_directory))
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
96 def get_relative_path(self, base_directory: Path) -> Path:
97 """Get the path relative to a base directory.
99 Args:
100 base_directory: The base directory to calculate relative path from
102 Returns:
103 Path relative to the base directory
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
112 @classmethod
113 def from_name_and_directory(cls, name: str, directory: Path) -> Self:
114 """Create a TemplatePath from template name and directory.
116 Args:
117 name: Template name (without .md extension)
118 directory: Directory containing the template
120 Returns:
121 New TemplatePath instance
123 Raises:
124 TemplateNotFoundError: If the template file does not exist
125 TemplateValidationError: If the path is invalid
127 """
128 template_file = directory / f'{name}.md' if not name.endswith('.md') else directory / name
130 return cls(template_file)
132 def __str__(self) -> str:
133 """String representation of the template path."""
134 return str(self._path)
136 def __repr__(self) -> str:
137 """Developer representation of the template path."""
138 return f'TemplatePath({self._path!r})'
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
146 def __hash__(self) -> int:
147 """Hash based on the path value."""
148 return hash(self._path)