Coverage for openhcs/core/path_cache.py: 0.0%

102 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +0000

1""" 

2Unified Path Cache System 

3 

4Provides shared path caching functionality for both TUI and PyQt GUI implementations. 

5Persists last used paths across application runs for improved user experience. 

6""" 

7 

8import json 

9import logging 

10from pathlib import Path 

11from typing import Dict, Optional 

12from enum import Enum 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17class PathCacheKey(Enum): 

18 """ 

19 Enumeration of path cache keys for different UI contexts. 

20  

21 Used to maintain separate cached paths for different file operations 

22 across both TUI and PyQt GUI implementations. 

23 """ 

24 # Original keys from both implementations 

25 FILE_SELECTION = "file_selection" 

26 DIRECTORY_SELECTION = "directory_selection" 

27 PLATE_IMPORT = "plate_import" 

28 CONFIG_EXPORT = "config_export" 

29 GENERAL = "general" 

30 

31 # Specific file type contexts 

32 FUNCTION_PATTERNS = "function_patterns" # .func files 

33 PIPELINE_FILES = "pipeline_files" # .pipeline files 

34 STEP_SETTINGS = "step_settings" # .step files 

35 DEBUG_FILES = "debug_files" # .pkl debug files 

36 CODE_EDITOR = "code_editor" # .py files from code editor 

37 

38 # Additional contexts for future use 

39 PLATE_BROWSER = "plate_browser" 

40 FUNCTION_BROWSER = "function_browser" 

41 PIPELINE_BROWSER = "pipeline_browser" 

42 EXPORT_BROWSER = "export_browser" 

43 CONFIG_BROWSER = "config_browser" 

44 ANALYSIS_BROWSER = "analysis_browser" 

45 

46 

47class UnifiedPathCache: 

48 """ 

49 Unified path cache for persisting directory paths across application sessions. 

50  

51 Provides consistent caching behavior for both TUI browser widgets and 

52 PyQt GUI file dialogs. 

53 """ 

54 

55 def __init__(self, cache_file: Optional[Path] = None): 

56 """ 

57 Initialize path cache. 

58 

59 Args: 

60 cache_file: Optional custom cache file location 

61 """ 

62 if cache_file is None: 

63 from openhcs.core.xdg_paths import get_data_file_path 

64 cache_file = get_data_file_path("path_cache.json") 

65 

66 self.cache_file = cache_file 

67 self._cache: Dict[str, str] = {} 

68 self._load_cache() 

69 logger.debug(f"UnifiedPathCache initialized with cache file: {self.cache_file}") 

70 

71 def _load_cache(self) -> None: 

72 """Load cache from disk.""" 

73 try: 

74 if self.cache_file.exists(): 

75 with open(self.cache_file, 'r') as f: 

76 self._cache = json.load(f) 

77 logger.debug(f"Loaded path cache with {len(self._cache)} entries") 

78 else: 

79 logger.debug("No existing path cache found, starting fresh") 

80 except (json.JSONDecodeError, OSError) as e: 

81 logger.warning(f"Failed to load path cache: {e}") 

82 self._cache = {} 

83 

84 def _save_cache(self) -> None: 

85 """Save cache to disk.""" 

86 try: 

87 # Ensure cache directory exists 

88 self.cache_file.parent.mkdir(parents=True, exist_ok=True) 

89 

90 with open(self.cache_file, 'w') as f: 

91 json.dump(self._cache, f, indent=2) 

92 logger.debug(f"Saved path cache with {len(self._cache)} entries") 

93 except OSError as e: 

94 logger.warning(f"Failed to save path cache: {e}") 

95 

96 def get_cached_path(self, key: PathCacheKey) -> Optional[Path]: 

97 """ 

98 Get cached path for a specific key. 

99  

100 Args: 

101 key: PathCacheKey identifying the context 

102  

103 Returns: 

104 Cached Path if exists and valid, None otherwise 

105 """ 

106 cached_str = self._cache.get(key.value) 

107 if cached_str: 

108 cached_path = Path(cached_str) 

109 if cached_path.exists(): 

110 logger.debug(f"Retrieved cached path for {key.value}: {cached_path}") 

111 return cached_path 

112 else: 

113 # Remove invalid cached path 

114 logger.debug(f"Removing invalid cached path for {key.value}: {cached_path}") 

115 del self._cache[key.value] 

116 self._save_cache() 

117 

118 return None 

119 

120 def set_cached_path(self, key: PathCacheKey, path: Path) -> None: 

121 """ 

122 Set cached path for a specific key. 

123  

124 Args: 

125 key: PathCacheKey identifying the context 

126 path: Path to cache 

127 """ 

128 if path and path.exists(): 

129 self._cache[key.value] = str(path) 

130 self._save_cache() 

131 logger.debug(f"Cached path for {key.value}: {path}") 

132 else: 

133 logger.warning(f"Attempted to cache non-existent path for {key.value}: {path}") 

134 

135 def get_initial_path(self, key: PathCacheKey, fallback: Optional[Path] = None) -> Path: 

136 """ 

137 Get initial path with intelligent fallback hierarchy. 

138  

139 Args: 

140 key: PathCacheKey identifying the context 

141 fallback: Optional fallback path if cached path unavailable 

142  

143 Returns: 

144 Best available path (cached > fallback > home directory) 

145 """ 

146 # Try cached path first 

147 cached = self.get_cached_path(key) 

148 if cached: 

149 return cached 

150 

151 # Try fallback 

152 if fallback and fallback.exists(): 

153 return fallback 

154 

155 # Ultimate fallback to home directory 

156 return Path.home() 

157 

158 def clear_cache(self) -> None: 

159 """Clear all cached paths.""" 

160 self._cache.clear() 

161 self._save_cache() 

162 logger.info("Cleared all cached paths") 

163 

164 def remove_cached_path(self, key: PathCacheKey) -> None: 

165 """ 

166 Remove specific cached path. 

167  

168 Args: 

169 key: PathCacheKey to remove 

170 """ 

171 if key.value in self._cache: 

172 del self._cache[key.value] 

173 self._save_cache() 

174 logger.debug(f"Removed cached path for {key.value}") 

175 

176 

177# Global cache instance 

178_global_path_cache: Optional[UnifiedPathCache] = None 

179 

180 

181def get_path_cache() -> UnifiedPathCache: 

182 """Get global path cache instance.""" 

183 global _global_path_cache 

184 if _global_path_cache is None: 

185 _global_path_cache = UnifiedPathCache() 

186 return _global_path_cache 

187 

188 

189def cache_path(key: PathCacheKey, path: Path) -> None: 

190 """ 

191 Convenience function to cache a path. 

192  

193 Args: 

194 key: PathCacheKey identifying the context 

195 path: Path to cache 

196 """ 

197 get_path_cache().set_cached_path(key, path) 

198 

199 

200def get_cached_path(key: PathCacheKey) -> Optional[Path]: 

201 """ 

202 Convenience function to get cached path. 

203  

204 Args: 

205 key: PathCacheKey identifying the context 

206  

207 Returns: 

208 Cached Path if exists and valid, None otherwise 

209 """ 

210 return get_path_cache().get_cached_path(key) 

211 

212 

213def get_initial_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path: 

214 """ 

215 Convenience function to get initial path with fallback. 

216  

217 Args: 

218 key: PathCacheKey identifying the context 

219 fallback: Optional fallback path 

220  

221 Returns: 

222 Best available path (cached > fallback > home directory) 

223 """ 

224 return get_path_cache().get_initial_path(key, fallback) 

225 

226 

227# Backward compatibility aliases for existing code 

228def cache_browser_path(key: PathCacheKey, path: Path) -> None: 

229 """Backward compatibility alias for TUI code.""" 

230 cache_path(key, path) 

231 

232 

233def cache_dialog_path(key: PathCacheKey, path: Path) -> None: 

234 """Backward compatibility alias for PyQt code.""" 

235 cache_path(key, path) 

236 

237 

238def get_cached_browser_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path: 

239 """Backward compatibility alias for TUI code.""" 

240 return get_initial_path(key, fallback) 

241 

242 

243def get_cached_dialog_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path: 

244 """Backward compatibility alias for PyQt code.""" 

245 return get_initial_path(key, fallback)