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

1"""Domain exceptions for the freewriting feature. 

2 

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

7 

8from __future__ import annotations 

9 

10# Constants for error message formatting 

11_PREVIEW_MAX_LENGTH = 50 

12 

13 

14class FreewriteError(Exception): 

15 """Base exception for all freewrite domain errors. 

16 

17 This is the root exception type for all freewriting-related errors. 

18 It provides a common base for catching any freewriting error. 

19 """ 

20 

21 def __init__(self, message: str, context: dict[str, str] | None = None) -> None: 

22 """Initialize the error with message and optional context. 

23 

24 Args: 

25 message: Human-readable error description. 

26 context: Optional dictionary with error context details. 

27 

28 """ 

29 super().__init__(message) 

30 self.message = message 

31 self.context = context or {} 

32 

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 

39 

40 

41class ValidationError(FreewriteError): 

42 """Raised when validation fails. 

43 

44 This exception is raised when user input or configuration 

45 fails domain validation rules. 

46 """ 

47 

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. 

56 

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. 

62 

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 

69 

70 

71class FileSystemError(FreewriteError): 

72 """Raised when file system operations fail. 

73 

74 This exception is raised when file operations (reading, writing, 

75 creating directories) encounter errors. 

76 """ 

77 

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. 

86 

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. 

92 

93 """ 

94 message = f'File system operation failed: {operation} on {file_path}' 

95 if system_error: 

96 message += f' ({system_error})' 

97 

98 super().__init__(message, context) 

99 self.operation = operation 

100 self.file_path = file_path 

101 self.system_error = system_error 

102 

103 

104class NodeError(FreewriteError): 

105 """Raised when node operations fail. 

106 

107 This exception is raised when operations on prosemark nodes 

108 encounter errors, such as invalid UUIDs or missing nodes. 

109 """ 

110 

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. 

119 

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. 

125 

126 """ 

127 node_desc = node_uuid or 'invalid-uuid' 

128 message = f'Node operation failed: {operation} on {node_desc} - {reason}' 

129 

130 super().__init__(message, context) 

131 self.node_uuid = node_uuid 

132 self.operation = operation 

133 self.reason = reason 

134 

135 

136class SessionError(FreewriteError): 

137 """Raised when session operations fail. 

138 

139 This exception is raised when session management operations 

140 encounter errors, such as invalid state transitions or 

141 configuration problems. 

142 """ 

143 

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. 

152 

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. 

158 

159 """ 

160 session_desc = session_id or 'unknown-session' 

161 message = f'Session operation failed: {operation} on {session_desc} - {reason}' 

162 

163 super().__init__(message, context) 

164 self.session_id = session_id 

165 self.operation = operation 

166 self.reason = reason 

167 

168 

169class TUIError(FreewriteError): 

170 """Raised when TUI operations fail. 

171 

172 This exception is raised when terminal user interface 

173 operations encounter errors that cannot be recovered from 

174 within the TUI. 

175 """ 

176 

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. 

186 

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. 

193 

194 """ 

195 message = f'TUI operation failed: {operation} in {component} - {reason}' 

196 

197 super().__init__(message, context) 

198 self.component = component 

199 self.operation = operation 

200 self.reason = reason 

201 self.recoverable = recoverable 

202 

203 

204class CLIError(FreewriteError): 

205 """Raised when CLI operations fail. 

206 

207 This exception is raised when command-line interface 

208 operations encounter errors, such as invalid arguments 

209 or configuration problems. 

210 """ 

211 

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. 

221 

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. 

228 

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}' 

234 

235 super().__init__(message, context) 

236 self.command = command 

237 self.argument = argument 

238 self.reason = reason 

239 self.exit_code = exit_code 

240 

241 def __str__(self) -> str: 

242 """Return string representation of the error. 

243 

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 

248 

249 

250class ConfigurationError(FreewriteError): 

251 """Raised when configuration is invalid. 

252 

253 This exception is raised when session configuration 

254 or system configuration contains invalid values or 

255 conflicting settings. 

256 """ 

257 

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. 

266 

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. 

272 

273 """ 

274 message = f'Invalid configuration: {config_key}={config_value} - {reason}' 

275 

276 super().__init__(message, context) 

277 self.config_key = config_key 

278 self.config_value = config_value 

279 self.reason = reason 

280 

281 

282class ContentError(FreewriteError): 

283 """Raised when content processing fails. 

284 

285 This exception is raised when operations on user content 

286 encounter errors, such as encoding issues or content 

287 validation failures. 

288 """ 

289 

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. 

298 

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. 

304 

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}' 

313 

314 super().__init__(message, context) 

315 self.operation = operation 

316 self.content_preview = content_preview 

317 self.reason = reason 

318 

319 

320# Legacy exception aliases for backward compatibility 

321# These can be removed once all code uses the new exception hierarchy 

322 

323 

324class ArgumentValidationError(CLIError): 

325 """Legacy alias for CLI argument validation errors.""" 

326 

327 def __init__(self, argument: str, _value: str, reason: str) -> None: 

328 """Initialize with CLI error pattern.""" 

329 super().__init__('validate', argument, reason) 

330 

331 

332class ThemeNotFoundError(CLIError): 

333 """Legacy alias for CLI theme not found errors.""" 

334 

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) 

338 

339 

340class DirectoryNotWritableError(CLIError): 

341 """Legacy alias for CLI directory not writable errors.""" 

342 

343 def __init__(self, operation: str, path: str, reason: str) -> None: 

344 """Initialize with CLI error pattern.""" 

345 super().__init__(operation, path, reason)