Coverage for src / tracekit / core / exceptions.py: 100%

137 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""TraceKit exception hierarchy with helpful error messages. 

2 

3This module provides custom exception classes that follow a consistent 

4template for error messages: WHAT, WHY, HOW TO FIX, DOCUMENTATION LINK. 

5 

6 

7Example: 

8 >>> try: 

9 ... raise UnsupportedFormatError(".xyz", ["wfm", "csv", "npz"]) 

10 ... except UnsupportedFormatError as e: 

11 ... print(e) 

12 Unsupported file format: .xyz 

13 ... 

14""" 

15 

16from __future__ import annotations 

17 

18from typing import Any 

19 

20# Documentation base URL 

21# Points to GitHub repository docs directory as primary documentation source. 

22# Update this when official documentation hosting is configured (e.g., ReadTheDocs). 

23# 

24# To verify/update this URL: 

25# 1. Check if https://tracekit.readthedocs.io exists and is active 

26# 2. If not, use GitHub repository docs: https://github.com/lair-click-bats/tracekit/tree/main/docs 

27# 3. Or local docs path for development: file:///path/to/docs/ 

28DOCS_BASE_URL = "https://github.com/lair-click-bats/tracekit/tree/main/docs" 

29 

30 

31class TraceKitError(Exception): 

32 """Base exception for all TraceKit errors. 

33 

34 All TraceKit exceptions inherit from this class, providing a 

35 consistent interface for error handling. 

36 

37 Attributes: 

38 message: Brief description of the error. 

39 details: Additional context about the error. 

40 fix_hint: Suggestion for how to fix the error. 

41 docs_path: Path to relevant documentation. 

42 

43 Example: 

44 >>> raise TraceKitError("Something went wrong") 

45 TraceKitError: Something went wrong 

46 """ 

47 

48 docs_path: str = "errors" 

49 

50 def __init__( 

51 self, 

52 message: str, 

53 *, 

54 details: str | None = None, 

55 fix_hint: str | None = None, 

56 docs_path: str | None = None, 

57 ) -> None: 

58 """Initialize TraceKitError. 

59 

60 Args: 

61 message: Brief description of the error. 

62 details: Additional context about what caused the error. 

63 fix_hint: Suggestion for how to fix the error. 

64 docs_path: Path to relevant documentation (appended to base URL). 

65 """ 

66 self.message = message 

67 self.details = details 

68 self.fix_hint = fix_hint 

69 if docs_path is not None: 

70 self.docs_path = docs_path 

71 super().__init__(self._format_message()) 

72 

73 def _format_message(self) -> str: 

74 """Format the full error message with template. 

75 

76 Returns: 

77 Formatted error message with WHAT, WHY, HOW TO FIX, DOCS. 

78 """ 

79 parts = [self.message] 

80 

81 if self.details: 

82 parts.append(f"Details: {self.details}") 

83 

84 if self.fix_hint: 

85 parts.append(f"Fix: {self.fix_hint}") 

86 

87 parts.append(f"Docs: {DOCS_BASE_URL}/{self.docs_path}") 

88 

89 return "\n".join(parts) 

90 

91 

92class LoaderError(TraceKitError): 

93 """Error loading trace data from file. 

94 

95 Raised when a file cannot be read, parsed, or converted to 

96 a TraceKit data structure. 

97 

98 Attributes: 

99 file_path: Path to the file that failed to load. 

100 """ 

101 

102 docs_path: str = "errors#loader" 

103 

104 def __init__( 

105 self, 

106 message: str, 

107 *, 

108 file_path: str | None = None, 

109 details: str | None = None, 

110 fix_hint: str | None = None, 

111 ) -> None: 

112 """Initialize LoaderError. 

113 

114 Args: 

115 message: Brief description of the error. 

116 file_path: Path to the file that failed to load. 

117 details: Additional context about the error. 

118 fix_hint: Suggestion for how to fix the error. 

119 """ 

120 self.file_path = file_path 

121 if file_path and not details: 

122 details = f"File: {file_path}" 

123 elif file_path and details: 

124 details = f"File: {file_path}. {details}" 

125 super().__init__( 

126 message, 

127 details=details, 

128 fix_hint=fix_hint, 

129 docs_path=self.docs_path, 

130 ) 

131 

132 

133class UnsupportedFormatError(LoaderError): 

134 """File format not recognized or unsupported. 

135 

136 Raised when attempting to load a file with an unsupported 

137 extension or format. 

138 

139 Attributes: 

140 format_ext: The unsupported format extension. 

141 supported_formats: List of supported format extensions. 

142 """ 

143 

144 docs_path: str = "errors#unsupported-format" 

145 

146 def __init__( 

147 self, 

148 format_ext: str, 

149 supported_formats: list[str] | None = None, 

150 *, 

151 file_path: str | None = None, 

152 ) -> None: 

153 """Initialize UnsupportedFormatError. 

154 

155 Args: 

156 format_ext: The unsupported format extension (e.g., ".xyz"). 

157 supported_formats: List of supported format extensions. 

158 file_path: Path to the file that failed to load. 

159 """ 

160 self.format_ext = format_ext 

161 self.supported_formats = supported_formats or [] 

162 

163 message = f"Unsupported file format: {format_ext}" 

164 

165 if self.supported_formats: 

166 formats_str = ", ".join(self.supported_formats) 

167 details = f"Supported formats: {formats_str}" 

168 else: 

169 details = None 

170 

171 fix_hint = "Convert the file to a supported format or use a custom loader." 

172 

173 super().__init__( 

174 message, 

175 file_path=file_path, 

176 details=details, 

177 fix_hint=fix_hint, 

178 ) 

179 

180 

181class FormatError(LoaderError): 

182 """File format is invalid or corrupted. 

183 

184 Raised when a file has the correct extension but invalid content. 

185 """ 

186 

187 docs_path: str = "errors#format-error" 

188 

189 def __init__( 

190 self, 

191 message: str, 

192 *, 

193 file_path: str | None = None, 

194 expected: str | None = None, 

195 got: str | None = None, 

196 details: str | None = None, 

197 fix_hint: str | None = None, 

198 ) -> None: 

199 """Initialize FormatError. 

200 

201 Args: 

202 message: Brief description of the error. 

203 file_path: Path to the file that failed to load. 

204 expected: What was expected in the file. 

205 got: What was actually found. 

206 details: Additional context about the error (overrides expected/got). 

207 fix_hint: Suggestion for how to fix the error. 

208 """ 

209 # Build details from expected/got if not provided directly 

210 if details is None: 

211 if expected and got: 

212 details = f"Expected: {expected}. Got: {got}" 

213 elif expected: 

214 details = f"Expected: {expected}" 

215 elif got: 

216 details = f"Found: {got}" 

217 

218 # Use default fix_hint if not provided 

219 if fix_hint is None: 

220 fix_hint = "Verify the file is not corrupted and matches the expected format." 

221 

222 super().__init__( 

223 message, 

224 file_path=file_path, 

225 details=details, 

226 fix_hint=fix_hint, 

227 ) 

228 

229 

230class AnalysisError(TraceKitError): 

231 """Error during signal analysis. 

232 

233 Raised when an analysis function encounters invalid data 

234 or cannot compute a result. 

235 """ 

236 

237 docs_path: str = "errors#analysis" 

238 

239 def __init__( 

240 self, 

241 message: str, 

242 *, 

243 analysis_type: str | None = None, 

244 details: str | None = None, 

245 fix_hint: str | None = None, 

246 ) -> None: 

247 """Initialize AnalysisError. 

248 

249 Args: 

250 message: Brief description of the error. 

251 analysis_type: Type of analysis that failed (e.g., "rise_time"). 

252 details: Additional context about the error. 

253 fix_hint: Suggestion for how to fix the error. 

254 """ 

255 self.analysis_type = analysis_type 

256 if analysis_type and not details: 

257 details = f"Analysis: {analysis_type}" 

258 elif analysis_type and details: 

259 details = f"Analysis: {analysis_type}. {details}" 

260 super().__init__( 

261 message, 

262 details=details, 

263 fix_hint=fix_hint, 

264 docs_path=self.docs_path, 

265 ) 

266 

267 

268class InsufficientDataError(AnalysisError): 

269 """Not enough data points for the requested analysis. 

270 

271 Raised when a signal is too short or lacks sufficient 

272 features (edges, periods, etc.) for analysis. 

273 

274 Attributes: 

275 required: Minimum data points or features required. 

276 available: Actual data points or features available. 

277 """ 

278 

279 docs_path: str = "errors#insufficient-data" 

280 

281 def __init__( 

282 self, 

283 message: str, 

284 *, 

285 required: int | None = None, 

286 available: int | None = None, 

287 analysis_type: str | None = None, 

288 ) -> None: 

289 """Initialize InsufficientDataError. 

290 

291 Args: 

292 message: Brief description of the error. 

293 required: Minimum number of samples or features required. 

294 available: Actual number available. 

295 analysis_type: Type of analysis that failed. 

296 """ 

297 self.required = required 

298 self.available = available 

299 

300 details = None 

301 if required is not None and available is not None: 

302 details = f"Required: {required}. Available: {available}" 

303 elif required is not None: 

304 details = f"Minimum required: {required}" 

305 

306 fix_hint = "Acquire more data or reduce analysis window." 

307 

308 super().__init__( 

309 message, 

310 analysis_type=analysis_type, 

311 details=details, 

312 fix_hint=fix_hint, 

313 ) 

314 

315 

316class SampleRateError(AnalysisError): 

317 """Invalid or missing sample rate. 

318 

319 Raised when sample rate is invalid (zero, negative) or 

320 insufficient for the requested analysis. 

321 

322 Attributes: 

323 required_rate: Minimum sample rate required. 

324 actual_rate: Actual sample rate provided. 

325 """ 

326 

327 docs_path: str = "errors#sample-rate" 

328 

329 def __init__( 

330 self, 

331 message: str, 

332 *, 

333 required_rate: float | None = None, 

334 actual_rate: float | None = None, 

335 ) -> None: 

336 """Initialize SampleRateError. 

337 

338 Args: 

339 message: Brief description of the error. 

340 required_rate: Minimum sample rate required in Hz. 

341 actual_rate: Actual sample rate in Hz. 

342 """ 

343 self.required_rate = required_rate 

344 self.actual_rate = actual_rate 

345 

346 details = None 

347 if required_rate is not None and actual_rate is not None: 

348 details = f"Required: {required_rate:.2e} Hz. Got: {actual_rate:.2e} Hz" 

349 elif actual_rate is not None: 

350 details = f"Got: {actual_rate:.2e} Hz" 

351 

352 fix_hint = "Ensure sample_rate is positive and sufficient for the analysis." 

353 

354 super().__init__( 

355 message, 

356 details=details, 

357 fix_hint=fix_hint, 

358 ) 

359 

360 

361class ConfigurationError(TraceKitError): 

362 """Invalid configuration parameters. 

363 

364 Raised when configuration is invalid, missing required fields, 

365 or contains invalid values. 

366 

367 Attributes: 

368 config_key: The configuration key that is invalid. 

369 expected_type: Expected type or format. 

370 actual_value: The invalid value that was provided. 

371 """ 

372 

373 docs_path: str = "errors#configuration" 

374 

375 def __init__( 

376 self, 

377 message: str, 

378 *, 

379 config_key: str | None = None, 

380 expected_type: str | None = None, 

381 actual_value: Any = None, 

382 details: str | None = None, 

383 fix_hint: str | None = None, 

384 ) -> None: 

385 """Initialize ConfigurationError. 

386 

387 Args: 

388 message: Brief description of the error. 

389 config_key: The configuration key that is invalid. 

390 expected_type: Expected type or format. 

391 actual_value: The invalid value that was provided. 

392 details: Additional context about the error. 

393 fix_hint: Suggestion for how to fix the error. 

394 """ 

395 self.config_key = config_key 

396 self.expected_type = expected_type 

397 self.actual_value = actual_value 

398 

399 # Build details from parts if not provided directly 

400 if details is None: 

401 details_parts = [] 

402 if config_key: 

403 details_parts.append(f"Key: {config_key}") 

404 if expected_type: 

405 details_parts.append(f"Expected: {expected_type}") 

406 if actual_value is not None: 

407 details_parts.append(f"Got: {actual_value!r}") 

408 details = ". ".join(details_parts) if details_parts else None 

409 

410 if fix_hint is None: 

411 fix_hint = "Check configuration file and ensure all values are valid." 

412 

413 super().__init__( 

414 message, 

415 details=details, 

416 fix_hint=fix_hint, 

417 docs_path=self.docs_path, 

418 ) 

419 

420 

421class ValidationError(TraceKitError): 

422 """Data validation failed. 

423 

424 Raised when input data does not meet validation requirements. 

425 

426 Attributes: 

427 field: The field that failed validation. 

428 constraint: The constraint that was violated. 

429 value: The value that failed validation. 

430 """ 

431 

432 docs_path: str = "errors#validation" 

433 

434 def __init__( 

435 self, 

436 message: str, 

437 *, 

438 field: str | None = None, 

439 constraint: str | None = None, 

440 value: Any = None, 

441 ) -> None: 

442 """Initialize ValidationError. 

443 

444 Args: 

445 message: Brief description of the error. 

446 field: The field that failed validation. 

447 constraint: The constraint that was violated. 

448 value: The value that failed validation. 

449 """ 

450 self.field = field 

451 self.constraint = constraint 

452 self.value = value 

453 

454 details_parts = [] 

455 if field: 

456 details_parts.append(f"Field: {field}") 

457 if constraint: 

458 details_parts.append(f"Constraint: {constraint}") 

459 if value is not None: 

460 details_parts.append(f"Value: {value!r}") 

461 

462 details = ". ".join(details_parts) if details_parts else None 

463 fix_hint = "Ensure input data meets all validation requirements." 

464 

465 super().__init__( 

466 message, 

467 details=details, 

468 fix_hint=fix_hint, 

469 docs_path=self.docs_path, 

470 ) 

471 

472 

473class ExportError(TraceKitError): 

474 """Error exporting data. 

475 

476 Raised when data cannot be exported to the requested format. 

477 

478 Attributes: 

479 export_format: The format that failed. 

480 output_path: Path where export was attempted. 

481 """ 

482 

483 docs_path: str = "errors#export" 

484 

485 def __init__( 

486 self, 

487 message: str, 

488 *, 

489 export_format: str | None = None, 

490 output_path: str | None = None, 

491 details: str | None = None, 

492 ) -> None: 

493 """Initialize ExportError. 

494 

495 Args: 

496 message: Brief description of the error. 

497 export_format: The format that failed (e.g., "csv", "hdf5"). 

498 output_path: Path where export was attempted. 

499 details: Additional context about the error. 

500 """ 

501 self.export_format = export_format 

502 self.output_path = output_path 

503 

504 details_parts = [] 

505 if export_format: 

506 details_parts.append(f"Format: {export_format}") 

507 if output_path: 

508 details_parts.append(f"Path: {output_path}") 

509 if details: 

510 details_parts.append(details) 

511 

512 combined_details = ". ".join(details_parts) if details_parts else None 

513 fix_hint = "Check output path is writable and data is valid for export." 

514 

515 super().__init__( 

516 message, 

517 details=combined_details, 

518 fix_hint=fix_hint, 

519 docs_path=self.docs_path, 

520 ) 

521 

522 

523__all__ = [ 

524 "DOCS_BASE_URL", 

525 "AnalysisError", 

526 "ConfigurationError", 

527 "ExportError", 

528 "FormatError", 

529 "InsufficientDataError", 

530 "LoaderError", 

531 "SampleRateError", 

532 "TraceKitError", 

533 "UnsupportedFormatError", 

534 "ValidationError", 

535]