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

101 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +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 

37 # Additional contexts for future use 

38 PLATE_BROWSER = "plate_browser" 

39 FUNCTION_BROWSER = "function_browser" 

40 PIPELINE_BROWSER = "pipeline_browser" 

41 EXPORT_BROWSER = "export_browser" 

42 CONFIG_BROWSER = "config_browser" 

43 ANALYSIS_BROWSER = "analysis_browser" 

44 

45 

46class UnifiedPathCache: 

47 """ 

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

49  

50 Provides consistent caching behavior for both TUI browser widgets and 

51 PyQt GUI file dialogs. 

52 """ 

53 

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

55 """ 

56 Initialize path cache. 

57 

58 Args: 

59 cache_file: Optional custom cache file location 

60 """ 

61 if cache_file is None: 

62 from openhcs.core.xdg_paths import get_data_file_path 

63 cache_file = get_data_file_path("path_cache.json") 

64 

65 self.cache_file = cache_file 

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

67 self._load_cache() 

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

69 

70 def _load_cache(self) -> None: 

71 """Load cache from disk.""" 

72 try: 

73 if self.cache_file.exists(): 

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

75 self._cache = json.load(f) 

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

77 else: 

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

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

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

81 self._cache = {} 

82 

83 def _save_cache(self) -> None: 

84 """Save cache to disk.""" 

85 try: 

86 # Ensure cache directory exists 

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

88 

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

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

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

92 except OSError as e: 

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

94 

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

96 """ 

97 Get cached path for a specific key. 

98  

99 Args: 

100 key: PathCacheKey identifying the context 

101  

102 Returns: 

103 Cached Path if exists and valid, None otherwise 

104 """ 

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

106 if cached_str: 

107 cached_path = Path(cached_str) 

108 if cached_path.exists(): 

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

110 return cached_path 

111 else: 

112 # Remove invalid cached path 

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

114 del self._cache[key.value] 

115 self._save_cache() 

116 

117 return None 

118 

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

120 """ 

121 Set cached path for a specific key. 

122  

123 Args: 

124 key: PathCacheKey identifying the context 

125 path: Path to cache 

126 """ 

127 if path and path.exists(): 

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

129 self._save_cache() 

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

131 else: 

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

133 

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

135 """ 

136 Get initial path with intelligent fallback hierarchy. 

137  

138 Args: 

139 key: PathCacheKey identifying the context 

140 fallback: Optional fallback path if cached path unavailable 

141  

142 Returns: 

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

144 """ 

145 # Try cached path first 

146 cached = self.get_cached_path(key) 

147 if cached: 

148 return cached 

149 

150 # Try fallback 

151 if fallback and fallback.exists(): 

152 return fallback 

153 

154 # Ultimate fallback to home directory 

155 return Path.home() 

156 

157 def clear_cache(self) -> None: 

158 """Clear all cached paths.""" 

159 self._cache.clear() 

160 self._save_cache() 

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

162 

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

164 """ 

165 Remove specific cached path. 

166  

167 Args: 

168 key: PathCacheKey to remove 

169 """ 

170 if key.value in self._cache: 

171 del self._cache[key.value] 

172 self._save_cache() 

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

174 

175 

176# Global cache instance 

177_global_path_cache: Optional[UnifiedPathCache] = None 

178 

179 

180def get_path_cache() -> UnifiedPathCache: 

181 """Get global path cache instance.""" 

182 global _global_path_cache 

183 if _global_path_cache is None: 

184 _global_path_cache = UnifiedPathCache() 

185 return _global_path_cache 

186 

187 

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

189 """ 

190 Convenience function to cache a path. 

191  

192 Args: 

193 key: PathCacheKey identifying the context 

194 path: Path to cache 

195 """ 

196 get_path_cache().set_cached_path(key, path) 

197 

198 

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

200 """ 

201 Convenience function to get cached path. 

202  

203 Args: 

204 key: PathCacheKey identifying the context 

205  

206 Returns: 

207 Cached Path if exists and valid, None otherwise 

208 """ 

209 return get_path_cache().get_cached_path(key) 

210 

211 

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

213 """ 

214 Convenience function to get initial path with fallback. 

215  

216 Args: 

217 key: PathCacheKey identifying the context 

218 fallback: Optional fallback path 

219  

220 Returns: 

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

222 """ 

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

224 

225 

226# Backward compatibility aliases for existing code 

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

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

229 cache_path(key, path) 

230 

231 

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

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

234 cache_path(key, path) 

235 

236 

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

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

239 return get_initial_path(key, fallback) 

240 

241 

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

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

244 return get_initial_path(key, fallback)