Skip to content

Commit e75503d

Browse files
GeneAIclaude
andcommitted
feat: Add comprehensive custom exception classes
Exception System: - Add MemDocsError base class with suggestions - Add specific exceptions for all error scenarios: - ConfigurationError (invalid config, missing files) - APIError (Claude API failures with status code handling) - ValidationError (input validation failures) - ExtractionError (code parsing failures) - SummarizationError (AI summarization failures) - IndexingError (memory indexing failures) - EmbeddingError (embedding generation failures) - MCPServerError (MCP protocol errors) - SecurityError (path traversal, unauthorized access) - DependencyError (missing optional dependencies) Features: - Clear, actionable error messages - Helpful suggestions for resolution - Status code-aware API error handling - Security-focused path validation - Convenience functions (require_api_key, validate_file_path, etc.) Testing: - 25 comprehensive unit tests - 96% coverage on exceptions module - All tests passing This improves user experience with professional error handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6edae71 commit e75503d

3 files changed

Lines changed: 508 additions & 2 deletions

File tree

memdocs/__init__.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,45 @@
88
__author__ = "Patrick Roebuck"
99
__license__ = "Apache-2.0"
1010

11+
from memdocs.exceptions import (
12+
APIError,
13+
ConfigurationError,
14+
DependencyError,
15+
EmbeddingError,
16+
ExtractionError,
17+
IndexingError,
18+
MCPServerError,
19+
MemDocsError,
20+
SecurityError,
21+
SummarizationError,
22+
ValidationError,
23+
)
1124
from memdocs.schemas import (
1225
DocIntConfig,
13-
ReviewResult,
1426
DocumentIndex,
15-
Symbol,
1627
FeatureSummary,
28+
ReviewResult,
29+
Symbol,
1730
)
1831

1932
__all__ = [
2033
"__version__",
34+
# Schemas
2135
"DocIntConfig",
2236
"ReviewResult",
2337
"DocumentIndex",
2438
"Symbol",
2539
"FeatureSummary",
40+
# Exceptions
41+
"MemDocsError",
42+
"ConfigurationError",
43+
"APIError",
44+
"ValidationError",
45+
"ExtractionError",
46+
"SummarizationError",
47+
"IndexingError",
48+
"EmbeddingError",
49+
"MCPServerError",
50+
"SecurityError",
51+
"DependencyError",
2652
]

memdocs/exceptions.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""
2+
Custom exceptions for MemDocs.
3+
4+
Provides clear, actionable error messages with suggestions for resolution.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from pathlib import Path
10+
from typing import Any
11+
12+
13+
class MemDocsError(Exception):
14+
"""Base exception for all MemDocs errors."""
15+
16+
def __init__(self, message: str, suggestion: str | None = None):
17+
self.message = message
18+
self.suggestion = suggestion
19+
super().__init__(self._format_message())
20+
21+
def _format_message(self) -> str:
22+
"""Format error message with optional suggestion."""
23+
if self.suggestion:
24+
return f"{self.message}\n\n💡 Suggestion: {self.suggestion}"
25+
return self.message
26+
27+
28+
class ConfigurationError(MemDocsError):
29+
"""Configuration file is invalid or missing."""
30+
31+
def __init__(self, message: str, config_path: Path | None = None):
32+
suggestion = None
33+
if config_path:
34+
suggestion = f"Check your configuration at: {config_path}"
35+
else:
36+
suggestion = "Run 'memdocs init' to create a default configuration"
37+
super().__init__(message, suggestion)
38+
39+
40+
class APIError(MemDocsError):
41+
"""Error calling external API (Claude, OpenAI, etc.)."""
42+
43+
def __init__(
44+
self,
45+
message: str,
46+
provider: str = "Anthropic",
47+
status_code: int | None = None,
48+
):
49+
self.provider = provider
50+
self.status_code = status_code
51+
52+
suggestion = self._get_api_suggestion(status_code)
53+
super().__init__(message, suggestion)
54+
55+
def _get_api_suggestion(self, status_code: int | None) -> str:
56+
"""Get helpful suggestion based on status code."""
57+
if status_code == 401:
58+
return "Check your ANTHROPIC_API_KEY environment variable"
59+
elif status_code == 429:
60+
return "Rate limit exceeded. Wait a moment and try again"
61+
elif status_code == 500:
62+
return "API server error. Try again in a few minutes"
63+
elif status_code:
64+
return f"API returned status code {status_code}"
65+
else:
66+
return "Check your API key and internet connection"
67+
68+
69+
class FileNotFoundError(MemDocsError):
70+
"""Requested file or directory not found."""
71+
72+
def __init__(self, path: Path, context: str | None = None):
73+
message = f"File or directory not found: {path}"
74+
if context:
75+
message += f"\n{context}"
76+
77+
suggestion = None
78+
if path.suffix: # It's a file
79+
suggestion = f"Make sure '{path}' exists and is accessible"
80+
else: # It's a directory
81+
suggestion = f"Make sure directory '{path}' exists"
82+
83+
super().__init__(message, suggestion)
84+
85+
86+
class ValidationError(MemDocsError):
87+
"""Input validation failed."""
88+
89+
def __init__(self, field: str, value: Any, reason: str):
90+
message = f"Invalid value for '{field}': {value}\nReason: {reason}"
91+
suggestion = "Check the documentation for valid values"
92+
super().__init__(message, suggestion)
93+
94+
95+
class ExtractionError(MemDocsError):
96+
"""Failed to extract symbols or analyze code."""
97+
98+
def __init__(self, file_path: Path, reason: str):
99+
message = f"Failed to extract code from {file_path}: {reason}"
100+
suggestion = "Ensure the file is valid code and supported language"
101+
super().__init__(message, suggestion)
102+
103+
104+
class SummarizationError(MemDocsError):
105+
"""Failed to generate summary with AI."""
106+
107+
def __init__(self, reason: str):
108+
message = f"Failed to generate documentation summary: {reason}"
109+
suggestion = "Check your API key and try again with a smaller scope"
110+
super().__init__(message, suggestion)
111+
112+
113+
class IndexingError(MemDocsError):
114+
"""Failed to index or search memory."""
115+
116+
def __init__(self, operation: str, reason: str):
117+
message = f"Indexing operation '{operation}' failed: {reason}"
118+
suggestion = "Check memory directory permissions and disk space"
119+
super().__init__(message, suggestion)
120+
121+
122+
class EmbeddingError(MemDocsError):
123+
"""Failed to generate or use embeddings."""
124+
125+
def __init__(self, reason: str):
126+
message = f"Embedding operation failed: {reason}"
127+
suggestion = "Install embeddings support with: pip install memdocs[embeddings]"
128+
super().__init__(message, suggestion)
129+
130+
131+
class MCPServerError(MemDocsError):
132+
"""MCP server error."""
133+
134+
def __init__(self, operation: str, reason: str):
135+
message = f"MCP server error during '{operation}': {reason}"
136+
suggestion = "Check MCP server logs and configuration"
137+
super().__init__(message, suggestion)
138+
139+
140+
class SecurityError(MemDocsError):
141+
"""Security-related error (e.g., path traversal attempt)."""
142+
143+
def __init__(self, issue: str, path: Path | str):
144+
message = f"Security violation: {issue}\nPath: {path}"
145+
suggestion = "Only access files within your project directory"
146+
super().__init__(message, suggestion)
147+
148+
149+
class DependencyError(MemDocsError):
150+
"""Required dependency not installed."""
151+
152+
def __init__(self, package: str, feature: str):
153+
message = f"Required dependency '{package}' not installed for feature '{feature}'"
154+
suggestion = f"Install with: pip install memdocs[{feature}]"
155+
super().__init__(message, suggestion)
156+
157+
158+
# Convenience functions for common errors
159+
160+
161+
def require_api_key(provider: str = "Anthropic") -> None:
162+
"""Raise error if API key is not set."""
163+
import os
164+
165+
key_var = f"{provider.upper()}_API_KEY"
166+
if not os.environ.get(key_var):
167+
raise APIError(
168+
f"{provider} API key required but not found",
169+
provider=provider,
170+
status_code=401,
171+
)
172+
173+
174+
def validate_file_path(path: Path, must_exist: bool = True) -> None:
175+
"""Validate file path for security and existence."""
176+
# Security: Prevent path traversal
177+
try:
178+
path.resolve().relative_to(Path.cwd().resolve())
179+
except ValueError:
180+
raise SecurityError("Path traversal detected", path)
181+
182+
# Check existence
183+
if must_exist and not path.exists():
184+
raise FileNotFoundError(path)
185+
186+
187+
def validate_config_version(version: int, supported: list[int] = [1]) -> None:
188+
"""Validate configuration version."""
189+
if version not in supported:
190+
raise ConfigurationError(
191+
f"Unsupported configuration version: {version}. "
192+
f"Supported versions: {', '.join(map(str, supported))}"
193+
)

0 commit comments

Comments
 (0)