Files
markitect-main/markitect/cache_service.py
tegwick bbc6192fe1 refactor: Standardize error handling patterns across codebase
Comprehensive error handling improvements addressing inconsistent patterns:

• Created markitect/exceptions.py with complete domain-specific exception hierarchy
  - MarkitectError base class with context and cause chaining support
  - Specific exceptions for Document, AST, Cache, Database, Schema operations
  - Built-in logging and context preservation

• Fixed overly broad exception handling in tddai modules:
  - issue_fetcher.py: Replace generic Exception with specific Gitea errors
  - project_manager.py: Proper error translation with context preservation
  - coverage_analyzer.py: Replace silent suppression with logging

• Enhanced cache_service.py error handling:
  - Specific OSError/PermissionError handling for file operations
  - Logging integration for unexpected errors
  - Preserved error collection and reporting

• Implemented proper exception chaining patterns:
  - All error translations use `raise ... from e` for debugging
  - Preserved original exception context and stack traces
  - Added docstring declarations of raised exceptions

• Benefits:
  - Eliminates silent error suppression and debugging black holes
  - Provides specific, actionable error messages
  - Preserves full error context for troubleshooting
  - Establishes consistent patterns for future development

Resolves issue #21: Error handling standardization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 16:35:13 +02:00

240 lines
8.1 KiB
Python

"""
Cache directory service for MarkiTect - Convention over Configuration.
This module provides standardized cache directory resolution following best practices:
- Project-local cache in .ast_cache/ (like .git/, node_modules/)
- Respects XDG Base Directory Specification for system caches
- Provides fallback behavior for different environments
"""
import os
from pathlib import Path
from typing import Optional
class CacheDirectoryService:
"""
Service for resolving cache directory locations following conventions.
Convention over Configuration approach:
1. Project-local: .ast_cache/ in current working directory (default)
2. User cache: ~/.cache/markitect/ (XDG Base Directory compliant)
3. System temp: /tmp/markitect-cache/ (fallback)
"""
def get_cache_directory(self, prefer_local: bool = True) -> Path:
"""
Get cache directory following convention over configuration.
Args:
prefer_local: Whether to prefer project-local cache over user cache
Returns:
Path to cache directory (created if needed)
"""
if prefer_local:
# Project-local cache (like .git/, node_modules/)
cache_dir = Path.cwd() / ".ast_cache"
else:
# User cache following XDG Base Directory Specification
cache_dir = self._get_user_cache_directory()
# Ensure directory exists
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
def get_project_cache_directory(self) -> Path:
"""Get project-local cache directory (.ast_cache in current directory)."""
cache_dir = Path.cwd() / ".ast_cache"
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
def get_user_cache_directory(self) -> Path:
"""Get user cache directory following XDG Base Directory Specification."""
cache_dir = self._get_user_cache_directory()
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
def _get_user_cache_directory(self) -> Path:
"""Get user cache directory path (XDG compliant)."""
# Follow XDG Base Directory Specification
xdg_cache_home = os.environ.get('XDG_CACHE_HOME')
if xdg_cache_home:
return Path(xdg_cache_home) / "markitect"
else:
# Fallback: ~/.cache/markitect (Linux/macOS) or equivalent
return Path.home() / ".cache" / "markitect"
def find_cache_files(self, cache_dir: Optional[Path] = None) -> list[Path]:
"""
Find all AST cache files in specified or default cache directory.
Args:
cache_dir: Cache directory to search (defaults to project cache)
Returns:
List of cache file paths
"""
if cache_dir is None:
cache_dir = self.get_project_cache_directory()
if not cache_dir.exists():
return []
return list(cache_dir.glob("*.ast.json"))
def get_cache_stats(self, cache_dir: Optional[Path] = None) -> dict:
"""
Get cache statistics for specified or default cache directory.
Args:
cache_dir: Cache directory to analyze (defaults to project cache)
Returns:
Dictionary with cache statistics
"""
if cache_dir is None:
cache_dir = self.get_project_cache_directory()
cache_files = self.find_cache_files(cache_dir)
if not cache_files:
return {
'directory': str(cache_dir.absolute()),
'exists': cache_dir.exists(),
'total_files': 0,
'total_size': 0,
'size_formatted': '0 bytes'
}
total_size = sum(f.stat().st_size for f in cache_files)
# Format size in appropriate units
if total_size < 1024:
size_formatted = f"{total_size} bytes"
elif total_size < 1024 * 1024:
size_formatted = f"{total_size / 1024:.1f} KB"
else:
size_formatted = f"{total_size / (1024 * 1024):.1f} MB"
return {
'directory': str(cache_dir.absolute()),
'exists': cache_dir.exists(),
'total_files': len(cache_files),
'total_size': total_size,
'size_formatted': size_formatted,
'files': [str(f) for f in cache_files]
}
def clean_cache(self, cache_dir: Optional[Path] = None) -> dict:
"""
Clean all cache files from specified or default cache directory.
Args:
cache_dir: Cache directory to clean (defaults to project cache)
Returns:
Dictionary with cleaning results
"""
if cache_dir is None:
cache_dir = self.get_project_cache_directory()
if not cache_dir.exists():
return {
'success': True,
'message': 'Cache directory does not exist - nothing to clean',
'files_removed': 0
}
cache_files = self.find_cache_files(cache_dir)
if not cache_files:
return {
'success': True,
'message': 'Cache is already empty - nothing to clean',
'files_removed': 0
}
removed_count = 0
errors = []
for cache_file in cache_files:
try:
cache_file.unlink()
removed_count += 1
except (OSError, PermissionError) as e:
errors.append(f"Could not remove {cache_file}: {e}")
except Exception as e:
# Log unexpected errors but continue cleanup
import logging
logging.getLogger(__name__).warning(
f"Unexpected error removing cache file {cache_file}: {e}"
)
errors.append(f"Unexpected error removing {cache_file}: {e}")
if errors:
return {
'success': False,
'message': f"Removed {removed_count} files with {len(errors)} errors",
'files_removed': removed_count,
'errors': errors
}
else:
return {
'success': True,
'message': f"Cache cleaned successfully - removed {removed_count} file(s)",
'files_removed': removed_count
}
def invalidate_file_cache(self, file_path: str, cache_dir: Optional[Path] = None) -> dict:
"""
Invalidate cache for specific file.
Args:
file_path: Path to file whose cache should be invalidated
cache_dir: Cache directory to search (defaults to project cache)
Returns:
Dictionary with invalidation results
"""
if cache_dir is None:
cache_dir = self.get_project_cache_directory()
source_path = Path(file_path)
cache_filename = f"{source_path.name}.ast.json"
cache_file = cache_dir / cache_filename
if not cache_file.exists():
return {
'success': True,
'message': f'No cache found for {source_path.name} - nothing to invalidate',
'file_removed': False
}
try:
cache_file.unlink()
return {
'success': True,
'message': f'Cache invalidated for {source_path.name}',
'file_removed': True,
'cache_file': str(cache_file)
}
except (OSError, PermissionError) as e:
return {
'success': False,
'message': f'File system error removing cache for {source_path.name}: {e}',
'file_removed': False,
'error': str(e)
}
except Exception as e:
import logging
logging.getLogger(__name__).error(
f"Unexpected error removing cache for {source_path.name}: {e}",
exc_info=True
)
return {
'success': False,
'message': f'Unexpected error removing cache for {source_path.name}: {e}',
'file_removed': False,
'error': str(e)
}