Coverage for session_mgmt_mcp/utils/lazy_imports.py: 30.66%

115 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-01 05:22 -0700

1#!/usr/bin/env python3 

2"""Lazy loading utilities for optional dependencies. 

3 

4This module provides lazy loading for heavy or optional dependencies to improve 

5startup performance and handle missing dependencies gracefully. 

6""" 

7 

8import importlib 

9from collections.abc import Callable 

10from functools import wraps 

11from typing import Any, Never 

12 

13from .logging import get_session_logger 

14 

15logger = get_session_logger() 

16 

17 

18class LazyImport: 

19 """Lazy import wrapper that loads modules on first access.""" 

20 

21 def __init__( 

22 self, 

23 module_name: str, 

24 fallback_value: Any = None, 

25 import_error_msg: str | None = None, 

26 ) -> None: 

27 self.module_name = module_name 

28 self.fallback_value = fallback_value 

29 self.import_error_msg = import_error_msg 

30 self._module = None 

31 self._import_attempted = False 

32 self._import_failed = False 

33 

34 def __getattr__(self, name: str) -> Any: 

35 if not self._import_attempted: 

36 self._try_import() 

37 

38 if self._import_failed: 

39 if self.fallback_value is not None: 

40 return getattr(self.fallback_value, name, None) 

41 raise ImportError( 

42 self.import_error_msg or f"Module {self.module_name} not available", 

43 ) 

44 

45 return getattr(self._module, name) 

46 

47 def _try_import(self) -> None: 

48 """Attempt to import the module.""" 

49 self._import_attempted = True 

50 try: 

51 self._module = importlib.import_module(self.module_name) 

52 logger.debug(f"Successfully imported {self.module_name}") 

53 except ImportError as e: 

54 self._import_failed = True 

55 logger.warning(f"Failed to import {self.module_name}: {e}") 

56 

57 @property 

58 def available(self) -> bool: 

59 """Check if the module is available.""" 

60 if not self._import_attempted: 

61 self._try_import() 

62 return not self._import_failed 

63 

64 def __bool__(self) -> bool: 

65 if not self._import_attempted: 

66 self._try_import() 

67 return not self._import_failed 

68 

69 

70class LazyLoader: 

71 """Manages lazy loading of multiple optional dependencies.""" 

72 

73 def __init__(self) -> None: 

74 self._loaders: dict[str, LazyImport] = {} 

75 

76 def add_import( 

77 self, 

78 name: str, 

79 module_name: str, 

80 fallback_value: Any = None, 

81 error_msg: str | None = None, 

82 ) -> LazyImport: 

83 """Add a lazy import.""" 

84 loader = LazyImport(module_name, fallback_value, error_msg) 

85 self._loaders[name] = loader 

86 return loader 

87 

88 def get_import(self, name: str) -> LazyImport | None: 

89 """Get a lazy import by name.""" 

90 return self._loaders.get(name) 

91 

92 def check_availability(self) -> dict[str, bool]: 

93 """Check availability of all registered imports.""" 

94 return {name: loader.available for name, loader in self._loaders.items()} 

95 

96 

97# Global lazy loader instance 

98lazy_loader = LazyLoader() 

99 

100# Common lazy imports for session-mgmt-mcp 

101transformers = lazy_loader.add_import( 

102 "transformers", 

103 "transformers", 

104 error_msg="Transformers not available. Install with: uv sync --extra embeddings", 

105) 

106 

107onnxruntime = lazy_loader.add_import( 

108 "onnxruntime", 

109 "onnxruntime", 

110 error_msg="ONNX Runtime not available. Install with: uv sync --extra embeddings", 

111) 

112 

113tiktoken = lazy_loader.add_import( 

114 "tiktoken", 

115 "tiktoken", 

116 error_msg="tiktoken not available. Install with: uv sync --extra embeddings", 

117) 

118 

119duckdb = lazy_loader.add_import( 

120 "duckdb", 

121 "duckdb", 

122 error_msg="DuckDB not available. Install with: uv add duckdb", 

123) 

124 

125numpy = lazy_loader.add_import( 

126 "numpy", 

127 "numpy", 

128 error_msg="NumPy not available. Install with: uv sync --extra embeddings", 

129) 

130 

131 

132def require_dependency(dependency_name: str, install_hint: str | None = None): 

133 """Decorator to require a specific dependency for a function.""" 

134 

135 def decorator(func: Callable) -> Callable: 

136 @wraps(func) 

137 def wrapper(*args, **kwargs): 

138 loader = lazy_loader.get_import(dependency_name) 

139 if not loader or not loader.available: 

140 error_msg = f"Function {func.__name__} requires {dependency_name}" 

141 if install_hint: 

142 error_msg += f". Install with: {install_hint}" 

143 raise ImportError(error_msg) 

144 return func(*args, **kwargs) 

145 

146 return wrapper 

147 

148 return decorator 

149 

150 

151def optional_dependency(dependency_name: str, fallback_result: Any = None): 

152 """Decorator to handle optional dependencies gracefully.""" 

153 

154 def decorator(func: Callable) -> Callable: 

155 @wraps(func) 

156 def wrapper(*args, **kwargs): 

157 loader = lazy_loader.get_import(dependency_name) 

158 if not loader or not loader.available: 

159 logger.info( 

160 f"Function {func.__name__} skipped - {dependency_name} not available", 

161 ) 

162 return fallback_result 

163 return func(*args, **kwargs) 

164 

165 return wrapper 

166 

167 return decorator 

168 

169 

170class MockModule: 

171 """Mock module that provides fallback implementations.""" 

172 

173 def __init__(self, name: str) -> None: 

174 self.name = name 

175 

176 def __getattr__(self, name: str): 

177 def mock_function(*args, **kwargs) -> Never: 

178 msg = f"Mock function {name} called - {self.name} not available" 

179 raise ImportError( 

180 msg, 

181 ) 

182 

183 return mock_function 

184 

185 

186def create_embedding_mock(): 

187 """Create a mock for embedding functionality.""" 

188 

189 class MockEmbedding: 

190 def __init__(self, *args, **kwargs) -> None: 

191 pass 

192 

193 def encode(self, texts, *args, **kwargs): 

194 # Return random-like embeddings for testing 

195 import random 

196 

197 if isinstance(texts, str): 

198 return [[random.random() for _ in range(384)]] 

199 return [[random.random() for _ in range(384)] for _ in texts] 

200 

201 return MockEmbedding 

202 

203 

204def get_dependency_status() -> dict[str, dict[str, Any]]: 

205 """Get comprehensive status of all dependencies.""" 

206 status = {} 

207 

208 # Check core dependencies 

209 core_deps = ["duckdb"] 

210 for dep in core_deps: 

211 loader = lazy_loader.get_import(dep) 

212 status[dep] = { 

213 "available": loader.available if loader else False, 

214 "required": True, 

215 "category": "core", 

216 } 

217 

218 # Check optional dependencies 

219 optional_deps = ["transformers", "onnxruntime", "tiktoken", "numpy"] 

220 for dep in optional_deps: 

221 loader = lazy_loader.get_import(dep) 

222 status[dep] = { 

223 "available": loader.available if loader else False, 

224 "required": False, 

225 "category": "embeddings" 

226 if dep in ["transformers", "onnxruntime", "numpy"] 

227 else "optimization", 

228 } 

229 

230 # Overall status 

231 core_available = all(status[dep]["available"] for dep in core_deps) 

232 embeddings_available = all( 

233 status[dep]["available"] for dep in ["transformers", "onnxruntime", "numpy"] 

234 ) 

235 

236 status["_summary"] = { 

237 "core_functionality": core_available, 

238 "embedding_functionality": embeddings_available, 

239 "optimization_functionality": status["tiktoken"]["available"], 

240 "overall_health": core_available, 

241 } 

242 

243 return status 

244 

245 

246def log_dependency_status() -> None: 

247 """Log the current dependency status.""" 

248 status = get_dependency_status() 

249 summary = status["_summary"] 

250 

251 logger.info( 

252 "Dependency status check completed", 

253 core_functionality=summary["core_functionality"], 

254 embedding_functionality=summary["embedding_functionality"], 

255 optimization_functionality=summary["optimization_functionality"], 

256 ) 

257 

258 # Log missing dependencies 

259 missing_core = [dep for dep in ["duckdb"] if not status[dep]["available"]] 

260 missing_optional = [ 

261 dep 

262 for dep in ["transformers", "onnxruntime", "tiktoken", "numpy"] 

263 if not status[dep]["available"] 

264 ] 

265 

266 if missing_core: 

267 logger.warning("Missing core dependencies", missing=missing_core) 

268 

269 if missing_optional: 

270 logger.info( 

271 "Missing optional dependencies", 

272 missing=missing_optional, 

273 install_hint="uv sync --extra embeddings", 

274 ) 

275 

276 

277# Note: Dependency status logging should be called explicitly 

278# to avoid import-time issues. Call log_dependency_status() when needed.