Coverage for src/quickmcp/factory/errors.py: 68%
154 statements
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-20 20:56 +0200
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-20 20:56 +0200
1"""
2Error handling and custom exceptions for MCP Factory.
3"""
5from typing import List, Dict, Any
6import logging
8from .import_analyzer import MissingDependency
10logger = logging.getLogger(__name__)
13class FactoryError(Exception):
14 """Base exception for all factory-related errors."""
15 pass
18class ConfigurationError(FactoryError):
19 """Raised when there's an issue with factory configuration."""
20 pass
23class ModuleLoadError(FactoryError):
24 """Raised when a module cannot be loaded."""
25 def __init__(self, message: str, module_path: str, original_error: Exception = None):
26 super().__init__(message)
27 self.module_path = module_path
28 self.original_error = original_error
31class FunctionExtractionError(FactoryError):
32 """Raised when functions cannot be extracted from a module or class."""
33 pass
36class ToolRegistrationError(FactoryError):
37 """Raised when a tool cannot be registered."""
38 def __init__(self, message: str, tool_name: str, original_error: Exception = None):
39 super().__init__(message)
40 self.tool_name = tool_name
41 self.original_error = original_error
44class TypeConversionError(FactoryError):
45 """Raised when type conversion fails."""
46 def __init__(self, message: str, value: Any, target_type: type, parameter_name: str = None):
47 super().__init__(message)
48 self.value = value
49 self.target_type = target_type
50 self.parameter_name = parameter_name
53class MissingDependencyError(ImportError, FactoryError):
54 """Raised when required dependencies are missing."""
56 def __init__(self, message: str, missing_dependencies: List[MissingDependency], module_path: str = None):
57 super().__init__(message)
58 self.missing_dependencies = missing_dependencies
59 self.module_path = module_path
61 @property
62 def required_dependencies(self) -> List[MissingDependency]:
63 """Get only the required (non-optional) dependencies."""
64 return [dep for dep in self.missing_dependencies if dep.import_type != "optional"]
66 @property
67 def optional_dependencies(self) -> List[MissingDependency]:
68 """Get only the optional dependencies."""
69 return [dep for dep in self.missing_dependencies if dep.import_type == "optional"]
71 @property
72 def dev_dependencies(self) -> List[MissingDependency]:
73 """Get only the development dependencies."""
74 return [dep for dep in self.missing_dependencies if dep.is_dev_dependency]
76 def format_error_message(self, include_dev_deps: bool = True) -> str:
77 """Format a detailed error message with installation suggestions."""
78 lines = [str(self)]
80 if not self.missing_dependencies:
81 return lines[0]
83 if self.module_path:
84 lines.append(f"\nModule: {self.module_path}")
86 lines.append("\nMissing dependencies:")
88 # Group dependencies
89 required = self.required_dependencies
90 optional = self.optional_dependencies
91 dev_deps = self.dev_dependencies if include_dev_deps else []
93 if required:
94 lines.append("\n❌ Required dependencies (will cause import errors):")
95 for dep in required:
96 lines.append(f" • {dep.module}")
97 if dep.source_line and dep.line_number:
98 lines.append(f" Line {dep.line_number}: {dep.source_line}")
100 install_cmd = self._get_install_command(dep)
101 lines.append(f" Install: {install_cmd}")
103 if optional:
104 lines.append("\n⚠️ Optional dependencies (handled gracefully):")
105 for dep in optional:
106 lines.append(f" • {dep.module}")
107 install_cmd = self._get_install_command(dep)
108 lines.append(f" Install: {install_cmd}")
110 if dev_deps and include_dev_deps:
111 lines.append("\n🛠️ Development dependencies:")
112 for dep in dev_deps:
113 lines.append(f" • {dep.module}")
114 install_cmd = self._get_install_command(dep)
115 lines.append(f" Install: {install_cmd}")
117 # Generate installation commands
118 import shutil
119 use_uv = shutil.which('uv') is not None
120 cmd = "uv pip install" if use_uv else "pip install"
122 if required:
123 required_installs = [dep.suggested_install or dep.module for dep in required]
124 lines.append(f"\n💡 Quick install (required): {cmd} {' '.join(set(required_installs))}")
126 if optional and not required:
127 optional_installs = [dep.suggested_install or dep.module for dep in optional]
128 lines.append(f"\n💡 Quick install (optional): {cmd} {' '.join(set(optional_installs))}")
130 return "\n".join(lines)
132 def _get_install_command(self, dep: MissingDependency, use_uv: bool = None) -> str:
133 """Get the install command for a dependency."""
134 import shutil
136 # Auto-detect uv if not specified
137 if use_uv is None:
138 use_uv = shutil.which('uv') is not None
140 pkg = dep.suggested_install or dep.module
141 cmd = "uv pip install" if use_uv else "pip install"
142 return f"{cmd} {pkg}"
144 def get_install_commands(self, include_optional: bool = False, include_dev: bool = False, use_uv: bool = None) -> Dict[str, str]:
145 """Get install commands for different dependency types."""
146 import shutil
148 # Auto-detect uv if not specified
149 if use_uv is None:
150 use_uv = shutil.which('uv') is not None
152 cmd = "uv pip install" if use_uv else "pip install"
153 commands = {}
155 # Required dependencies
156 required = self.required_dependencies
157 if required:
158 packages = set(dep.suggested_install or dep.module for dep in required)
159 commands["required"] = f"{cmd} {' '.join(packages)}"
161 # Optional dependencies
162 if include_optional:
163 optional = self.optional_dependencies
164 if optional:
165 packages = set(dep.suggested_install or dep.module for dep in optional)
166 commands["optional"] = f"{cmd} {' '.join(packages)}"
168 # Development dependencies
169 if include_dev:
170 dev_deps = self.dev_dependencies
171 if dev_deps:
172 packages = set(dep.suggested_install or dep.module for dep in dev_deps)
173 commands["dev"] = f"{cmd} {' '.join(packages)}"
175 return commands
178class SafetyError(FactoryError):
179 """Raised when a safety check fails."""
180 pass
183class CodeExecutionError(SafetyError):
184 """Raised when code execution is attempted but not allowed."""
185 def __init__(self, message: str, module_path: str):
186 super().__init__(message)
187 self.module_path = module_path
190def handle_factory_error(error: Exception, context: str = "", verbose: bool = True) -> str:
191 """Handle and format factory errors consistently."""
192 if isinstance(error, MissingDependencyError):
193 return error.format_error_message()
195 elif isinstance(error, ToolRegistrationError):
196 msg = f"Failed to register tool '{error.tool_name}'"
197 if context:
198 msg = f"{context}: {msg}"
199 if error.original_error and verbose:
200 msg = f"{msg}: {error.original_error}"
201 return msg
203 elif isinstance(error, ModuleLoadError):
204 msg = f"Failed to load module '{error.module_path}'"
205 if context:
206 msg = f"{context}: {msg}"
207 if error.original_error and verbose:
208 msg = f"{msg}: {error.original_error}"
209 return msg
211 elif isinstance(error, TypeConversionError):
212 msg = f"Type conversion failed"
213 if error.parameter_name:
214 msg = f"{msg} for parameter '{error.parameter_name}'"
215 msg = f"{msg}: cannot convert {type(error.value).__name__} to {error.target_type.__name__}"
216 if context:
217 msg = f"{context}: {msg}"
218 return msg
220 elif isinstance(error, FactoryError):
221 msg = str(error)
222 if context:
223 msg = f"{context}: {msg}"
224 return msg
226 else:
227 # Generic error handling
228 error_type = type(error).__name__
229 msg = f"{error_type}: {error}"
230 if context:
231 msg = f"{context}: {msg}"
232 return msg
235def log_factory_error(error: Exception, context: str = "", level: int = logging.ERROR):
236 """Log factory errors with appropriate detail level."""
237 message = handle_factory_error(error, context, verbose=True)
239 if isinstance(error, (MissingDependencyError, ConfigurationError)):
240 # These are expected errors that users should see
241 logger.log(level, message)
242 else:
243 # Unexpected errors should include traceback
244 logger.log(level, message, exc_info=True)