Coverage for src/prosemark/freewriting/domain/exceptions.py: 100%
88 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"""Domain exceptions for the freewriting feature.
3This module contains all the domain-specific exceptions that can be raised
4by the freewriting business logic. These exceptions represent business rule
5violations and error conditions in the domain layer.
6"""
8from __future__ import annotations
10# Constants for error message formatting
11_PREVIEW_MAX_LENGTH = 50
14class FreewriteError(Exception):
15 """Base exception for all freewrite domain errors.
17 This is the root exception type for all freewriting-related errors.
18 It provides a common base for catching any freewriting error.
19 """
21 def __init__(self, message: str, context: dict[str, str] | None = None) -> None:
22 """Initialize the error with message and optional context.
24 Args:
25 message: Human-readable error description.
26 context: Optional dictionary with error context details.
28 """
29 super().__init__(message)
30 self.message = message
31 self.context = context or {}
33 def __str__(self) -> str:
34 """Return string representation of the error."""
35 if self.context:
36 context_str = ', '.join(f'{k}={v}' for k, v in self.context.items())
37 return f'{self.message} ({context_str})'
38 return self.message
41class ValidationError(FreewriteError):
42 """Raised when validation fails.
44 This exception is raised when user input or configuration
45 fails domain validation rules.
46 """
48 def __init__(
49 self,
50 field_name: str,
51 field_value: str,
52 validation_rule: str,
53 context: dict[str, str] | None = None,
54 ) -> None:
55 """Initialize validation error with field details.
57 Args:
58 field_name: Name of the field that failed validation.
59 field_value: The invalid value that was provided.
60 validation_rule: Description of the validation rule that failed.
61 context: Optional additional context.
63 """
64 message = f'Validation failed for {field_name}: {validation_rule}'
65 super().__init__(message, context)
66 self.field_name = field_name
67 self.field_value = field_value
68 self.validation_rule = validation_rule
71class FileSystemError(FreewriteError):
72 """Raised when file system operations fail.
74 This exception is raised when file operations (reading, writing,
75 creating directories) encounter errors.
76 """
78 def __init__(
79 self,
80 operation: str,
81 file_path: str,
82 system_error: str | None = None,
83 context: dict[str, str] | None = None,
84 ) -> None:
85 """Initialize file system error with operation details.
87 Args:
88 operation: The file operation that failed (e.g., 'write', 'read').
89 file_path: Path to the file involved in the operation.
90 system_error: Optional system error message from OS.
91 context: Optional additional context.
93 """
94 message = f'File system operation failed: {operation} on {file_path}'
95 if system_error:
96 message += f' ({system_error})'
98 super().__init__(message, context)
99 self.operation = operation
100 self.file_path = file_path
101 self.system_error = system_error
104class NodeError(FreewriteError):
105 """Raised when node operations fail.
107 This exception is raised when operations on prosemark nodes
108 encounter errors, such as invalid UUIDs or missing nodes.
109 """
111 def __init__(
112 self,
113 node_uuid: str | None,
114 operation: str,
115 reason: str,
116 context: dict[str, str] | None = None,
117 ) -> None:
118 """Initialize node error with operation details.
120 Args:
121 node_uuid: UUID of the node involved (may be None for UUID validation errors).
122 operation: The node operation that failed.
123 reason: Reason why the operation failed.
124 context: Optional additional context.
126 """
127 node_desc = node_uuid or 'invalid-uuid'
128 message = f'Node operation failed: {operation} on {node_desc} - {reason}'
130 super().__init__(message, context)
131 self.node_uuid = node_uuid
132 self.operation = operation
133 self.reason = reason
136class SessionError(FreewriteError):
137 """Raised when session operations fail.
139 This exception is raised when session management operations
140 encounter errors, such as invalid state transitions or
141 configuration problems.
142 """
144 def __init__(
145 self,
146 session_id: str | None,
147 operation: str,
148 reason: str,
149 context: dict[str, str] | None = None,
150 ) -> None:
151 """Initialize session error with operation details.
153 Args:
154 session_id: ID of the session involved (may be None).
155 operation: The session operation that failed.
156 reason: Reason why the operation failed.
157 context: Optional additional context.
159 """
160 session_desc = session_id or 'unknown-session'
161 message = f'Session operation failed: {operation} on {session_desc} - {reason}'
163 super().__init__(message, context)
164 self.session_id = session_id
165 self.operation = operation
166 self.reason = reason
169class TUIError(FreewriteError):
170 """Raised when TUI operations fail.
172 This exception is raised when terminal user interface
173 operations encounter errors that cannot be recovered from
174 within the TUI.
175 """
177 def __init__(
178 self,
179 component: str,
180 operation: str,
181 reason: str,
182 recoverable: bool = True, # noqa: FBT001, FBT002
183 context: dict[str, str] | None = None,
184 ) -> None:
185 """Initialize TUI error with component details.
187 Args:
188 component: TUI component that encountered the error.
189 operation: The operation that failed.
190 reason: Reason why the operation failed.
191 recoverable: Whether the error can be recovered from.
192 context: Optional additional context.
194 """
195 message = f'TUI operation failed: {operation} in {component} - {reason}'
197 super().__init__(message, context)
198 self.component = component
199 self.operation = operation
200 self.reason = reason
201 self.recoverable = recoverable
204class CLIError(FreewriteError):
205 """Raised when CLI operations fail.
207 This exception is raised when command-line interface
208 operations encounter errors, such as invalid arguments
209 or configuration problems.
210 """
212 def __init__(
213 self,
214 command: str,
215 argument: str | None,
216 reason: str,
217 exit_code: int = 1,
218 context: dict[str, str] | None = None,
219 ) -> None:
220 """Initialize CLI error with command details.
222 Args:
223 command: CLI command that failed.
224 argument: Specific argument that caused the error (may be None).
225 reason: Reason why the command failed.
226 exit_code: Suggested exit code for the CLI.
227 context: Optional additional context.
229 """
230 if argument:
231 message = f'CLI command failed: {command} with argument {argument} - {reason}'
232 else:
233 message = f'CLI command failed: {command} - {reason}'
235 super().__init__(message, context)
236 self.command = command
237 self.argument = argument
238 self.reason = reason
239 self.exit_code = exit_code
241 def __str__(self) -> str:
242 """Return string representation of the error.
244 For CLI errors, we return just the reason to maintain backward compatibility
245 with the contract tests that expect simple reason strings.
246 """
247 return self.reason
250class ConfigurationError(FreewriteError):
251 """Raised when configuration is invalid.
253 This exception is raised when session configuration
254 or system configuration contains invalid values or
255 conflicting settings.
256 """
258 def __init__(
259 self,
260 config_key: str,
261 config_value: str,
262 reason: str,
263 context: dict[str, str] | None = None,
264 ) -> None:
265 """Initialize configuration error with config details.
267 Args:
268 config_key: Configuration key that is invalid.
269 config_value: The invalid configuration value.
270 reason: Reason why the configuration is invalid.
271 context: Optional additional context.
273 """
274 message = f'Invalid configuration: {config_key}={config_value} - {reason}'
276 super().__init__(message, context)
277 self.config_key = config_key
278 self.config_value = config_value
279 self.reason = reason
282class ContentError(FreewriteError):
283 """Raised when content processing fails.
285 This exception is raised when operations on user content
286 encounter errors, such as encoding issues or content
287 validation failures.
288 """
290 def __init__(
291 self,
292 operation: str,
293 content_preview: str,
294 reason: str,
295 context: dict[str, str] | None = None,
296 ) -> None:
297 """Initialize content error with operation details.
299 Args:
300 operation: Content operation that failed.
301 content_preview: First few characters of the problematic content.
302 reason: Reason why the operation failed.
303 context: Optional additional context.
305 """
306 # Limit preview to prevent huge error messages
307 preview = (
308 content_preview[:_PREVIEW_MAX_LENGTH] + '...'
309 if len(content_preview) > _PREVIEW_MAX_LENGTH
310 else content_preview
311 )
312 message = f'Content operation failed: {operation} on "{preview}" - {reason}'
314 super().__init__(message, context)
315 self.operation = operation
316 self.content_preview = content_preview
317 self.reason = reason
320# Legacy exception aliases for backward compatibility
321# These can be removed once all code uses the new exception hierarchy
324class ArgumentValidationError(CLIError):
325 """Legacy alias for CLI argument validation errors."""
327 def __init__(self, argument: str, _value: str, reason: str) -> None:
328 """Initialize with CLI error pattern."""
329 super().__init__('validate', argument, reason)
332class ThemeNotFoundError(CLIError):
333 """Legacy alias for CLI theme not found errors."""
335 def __init__(self, config_key: str, _value: str, reason: str) -> None:
336 """Initialize with CLI error pattern."""
337 super().__init__('configure', config_key, reason)
340class DirectoryNotWritableError(CLIError):
341 """Legacy alias for CLI directory not writable errors."""
343 def __init__(self, operation: str, path: str, reason: str) -> None:
344 """Initialize with CLI error pattern."""
345 super().__init__(operation, path, reason)