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
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2Unified Path Cache System
4Provides shared path caching functionality for both TUI and PyQt GUI implementations.
5Persists last used paths across application runs for improved user experience.
6"""
8import json
9import logging
10from pathlib import Path
11from typing import Dict, Optional
12from enum import Enum
14logger = logging.getLogger(__name__)
17class PathCacheKey(Enum):
18 """
19 Enumeration of path cache keys for different UI contexts.
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"
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
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"
46class UnifiedPathCache:
47 """
48 Unified path cache for persisting directory paths across application sessions.
50 Provides consistent caching behavior for both TUI browser widgets and
51 PyQt GUI file dialogs.
52 """
54 def __init__(self, cache_file: Optional[Path] = None):
55 """
56 Initialize path cache.
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")
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}")
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 = {}
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)
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}")
95 def get_cached_path(self, key: PathCacheKey) -> Optional[Path]:
96 """
97 Get cached path for a specific key.
99 Args:
100 key: PathCacheKey identifying the context
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()
117 return None
119 def set_cached_path(self, key: PathCacheKey, path: Path) -> None:
120 """
121 Set cached path for a specific key.
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}")
134 def get_initial_path(self, key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
135 """
136 Get initial path with intelligent fallback hierarchy.
138 Args:
139 key: PathCacheKey identifying the context
140 fallback: Optional fallback path if cached path unavailable
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
150 # Try fallback
151 if fallback and fallback.exists():
152 return fallback
154 # Ultimate fallback to home directory
155 return Path.home()
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")
163 def remove_cached_path(self, key: PathCacheKey) -> None:
164 """
165 Remove specific cached path.
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}")
176# Global cache instance
177_global_path_cache: Optional[UnifiedPathCache] = None
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
188def cache_path(key: PathCacheKey, path: Path) -> None:
189 """
190 Convenience function to cache a path.
192 Args:
193 key: PathCacheKey identifying the context
194 path: Path to cache
195 """
196 get_path_cache().set_cached_path(key, path)
199def get_cached_path(key: PathCacheKey) -> Optional[Path]:
200 """
201 Convenience function to get cached path.
203 Args:
204 key: PathCacheKey identifying the context
206 Returns:
207 Cached Path if exists and valid, None otherwise
208 """
209 return get_path_cache().get_cached_path(key)
212def get_initial_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
213 """
214 Convenience function to get initial path with fallback.
216 Args:
217 key: PathCacheKey identifying the context
218 fallback: Optional fallback path
220 Returns:
221 Best available path (cached > fallback > home directory)
222 """
223 return get_path_cache().get_initial_path(key, fallback)
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)
232def cache_dialog_path(key: PathCacheKey, path: Path) -> None:
233 """Backward compatibility alias for PyQt code."""
234 cache_path(key, path)
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)
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)