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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +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
36 CODE_EDITOR = "code_editor" # .py files from code editor
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"
47class UnifiedPathCache:
48 """
49 Unified path cache for persisting directory paths across application sessions.
51 Provides consistent caching behavior for both TUI browser widgets and
52 PyQt GUI file dialogs.
53 """
55 def __init__(self, cache_file: Optional[Path] = None):
56 """
57 Initialize path cache.
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")
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}")
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 = {}
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)
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}")
96 def get_cached_path(self, key: PathCacheKey) -> Optional[Path]:
97 """
98 Get cached path for a specific key.
100 Args:
101 key: PathCacheKey identifying the context
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()
118 return None
120 def set_cached_path(self, key: PathCacheKey, path: Path) -> None:
121 """
122 Set cached path for a specific key.
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}")
135 def get_initial_path(self, key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
136 """
137 Get initial path with intelligent fallback hierarchy.
139 Args:
140 key: PathCacheKey identifying the context
141 fallback: Optional fallback path if cached path unavailable
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
151 # Try fallback
152 if fallback and fallback.exists():
153 return fallback
155 # Ultimate fallback to home directory
156 return Path.home()
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")
164 def remove_cached_path(self, key: PathCacheKey) -> None:
165 """
166 Remove specific cached path.
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}")
177# Global cache instance
178_global_path_cache: Optional[UnifiedPathCache] = None
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
189def cache_path(key: PathCacheKey, path: Path) -> None:
190 """
191 Convenience function to cache a path.
193 Args:
194 key: PathCacheKey identifying the context
195 path: Path to cache
196 """
197 get_path_cache().set_cached_path(key, path)
200def get_cached_path(key: PathCacheKey) -> Optional[Path]:
201 """
202 Convenience function to get cached path.
204 Args:
205 key: PathCacheKey identifying the context
207 Returns:
208 Cached Path if exists and valid, None otherwise
209 """
210 return get_path_cache().get_cached_path(key)
213def get_initial_path(key: PathCacheKey, fallback: Optional[Path] = None) -> Path:
214 """
215 Convenience function to get initial path with fallback.
217 Args:
218 key: PathCacheKey identifying the context
219 fallback: Optional fallback path
221 Returns:
222 Best available path (cached > fallback > home directory)
223 """
224 return get_path_cache().get_initial_path(key, fallback)
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)
233def cache_dialog_path(key: PathCacheKey, path: Path) -> None:
234 """Backward compatibility alias for PyQt code."""
235 cache_path(key, path)
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)
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)