crackerjack 0.32.0__py3-none-any.whl → 0.33.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +64 -6
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +257 -218
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +558 -240
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +66 -13
- crackerjack/managers/test_command_builder.py +5 -17
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +109 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +161 -32
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +174 -33
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +15 -12
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +3 -0
- crackerjack/mixins/error_handling.py +145 -0
- crackerjack/models/config.py +21 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +176 -107
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/models/task.py +3 -0
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +90 -105
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +18 -11
- crackerjack/services/config_merge.py +30 -85
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +41 -17
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +41 -45
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +30 -33
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +409 -0
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +416 -0
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +250 -576
- crackerjack/services/performance_cache.py +382 -0
- crackerjack/services/performance_monitor.py +565 -0
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +61 -30
- crackerjack/services/security_logger.py +18 -22
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.32.0.dist-info/RECORD +0 -180
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,11 +11,16 @@ from rich.console import Console
|
|
|
11
11
|
|
|
12
12
|
from crackerjack.models.protocols import (
|
|
13
13
|
ConfigMergeServiceProtocol,
|
|
14
|
+
ConfigurationServiceProtocol,
|
|
15
|
+
CoverageRatchetProtocol,
|
|
14
16
|
FileSystemInterface,
|
|
15
17
|
GitInterface,
|
|
16
18
|
HookManager,
|
|
19
|
+
InitializationServiceProtocol,
|
|
17
20
|
PublishManager,
|
|
21
|
+
SecurityServiceProtocol,
|
|
18
22
|
TestManagerProtocol,
|
|
23
|
+
UnifiedConfigurationServiceProtocol,
|
|
19
24
|
)
|
|
20
25
|
from crackerjack.services.logging import get_logger
|
|
21
26
|
|
|
@@ -39,7 +44,7 @@ class ServiceDescriptor:
|
|
|
39
44
|
created_count: int = 0
|
|
40
45
|
dependencies: list[type] = field(default_factory=list)
|
|
41
46
|
|
|
42
|
-
def __post_init__(self):
|
|
47
|
+
def __post_init__(self) -> None:
|
|
43
48
|
if self.implementation is self.factory is self.instance is None:
|
|
44
49
|
msg = "Must provide either implementation, factory, or instance"
|
|
45
50
|
raise ValueError(msg)
|
|
@@ -110,7 +115,6 @@ class DependencyResolver:
|
|
|
110
115
|
dependency = self.container.get(param.annotation)
|
|
111
116
|
kwargs[param_name] = dependency
|
|
112
117
|
except Exception as e:
|
|
113
|
-
# Only log as warning for required parameters, debug for optional ones with defaults
|
|
114
118
|
if param.default == inspect.Parameter.empty:
|
|
115
119
|
self.logger.warning(
|
|
116
120
|
"Could not inject dependency",
|
|
@@ -142,7 +146,7 @@ class DependencyResolver:
|
|
|
142
146
|
|
|
143
147
|
def _build_constructor_kwargs(self, implementation: type) -> dict[str, Any]:
|
|
144
148
|
init_sig = inspect.signature(implementation.__init__)
|
|
145
|
-
kwargs = {}
|
|
149
|
+
kwargs: dict[str, Any] = {}
|
|
146
150
|
|
|
147
151
|
for param_name, param in init_sig.parameters.items():
|
|
148
152
|
if param_name == "self":
|
|
@@ -296,7 +300,7 @@ class EnhancedDependencyContainer:
|
|
|
296
300
|
self._current_scope = scope
|
|
297
301
|
|
|
298
302
|
def get_service_info(self) -> dict[str, Any]:
|
|
299
|
-
info = {}
|
|
303
|
+
info: dict[str, Any] = {}
|
|
300
304
|
|
|
301
305
|
with self._lock:
|
|
302
306
|
for key, descriptor in self._services.items():
|
|
@@ -387,7 +391,7 @@ class EnhancedDependencyContainer:
|
|
|
387
391
|
def _get_service_key(self, interface: type) -> str:
|
|
388
392
|
return f"{interface.__module__}.{interface.__name__}"
|
|
389
393
|
|
|
390
|
-
def __enter__(self):
|
|
394
|
+
def __enter__(self) -> "EnhancedDependencyContainer":
|
|
391
395
|
return self
|
|
392
396
|
|
|
393
397
|
def __exit__(
|
|
@@ -470,6 +474,56 @@ class ServiceCollectionBuilder:
|
|
|
470
474
|
|
|
471
475
|
return self
|
|
472
476
|
|
|
477
|
+
def add_service_protocols(self) -> "ServiceCollectionBuilder":
|
|
478
|
+
console = self.console or Console(force_terminal=True)
|
|
479
|
+
pkg_path = self.pkg_path or Path.cwd()
|
|
480
|
+
|
|
481
|
+
def create_coverage_ratchet() -> CoverageRatchetProtocol:
|
|
482
|
+
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
483
|
+
|
|
484
|
+
return CoverageRatchetService(pkg_path, console)
|
|
485
|
+
|
|
486
|
+
self.container.register_transient(
|
|
487
|
+
CoverageRatchetProtocol,
|
|
488
|
+
factory=create_coverage_ratchet,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def create_configuration_service() -> ConfigurationServiceProtocol:
|
|
492
|
+
from crackerjack.services.config import ConfigurationService
|
|
493
|
+
|
|
494
|
+
return ConfigurationService(console=console, pkg_path=pkg_path)
|
|
495
|
+
|
|
496
|
+
self.container.register_transient(
|
|
497
|
+
ConfigurationServiceProtocol,
|
|
498
|
+
factory=create_configuration_service,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def create_security_service() -> SecurityServiceProtocol:
|
|
502
|
+
from crackerjack.services.security import SecurityService
|
|
503
|
+
|
|
504
|
+
return SecurityService()
|
|
505
|
+
|
|
506
|
+
self.container.register_transient(
|
|
507
|
+
SecurityServiceProtocol,
|
|
508
|
+
factory=create_security_service,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def create_initialization_service() -> InitializationServiceProtocol:
|
|
512
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
513
|
+
from crackerjack.services.git import GitService
|
|
514
|
+
from crackerjack.services.initialization import InitializationService
|
|
515
|
+
|
|
516
|
+
filesystem = FileSystemService()
|
|
517
|
+
git_service = GitService(console, pkg_path)
|
|
518
|
+
return InitializationService(console, filesystem, git_service, pkg_path)
|
|
519
|
+
|
|
520
|
+
self.container.register_transient(
|
|
521
|
+
InitializationServiceProtocol,
|
|
522
|
+
factory=create_initialization_service,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return self
|
|
526
|
+
|
|
473
527
|
def add_configuration_services(self) -> "ServiceCollectionBuilder":
|
|
474
528
|
console = self.console or Console(force_terminal=True)
|
|
475
529
|
pkg_path = self.pkg_path or Path.cwd()
|
|
@@ -481,7 +535,11 @@ class ServiceCollectionBuilder:
|
|
|
481
535
|
factory=lambda: UnifiedConfigurationService(console, pkg_path),
|
|
482
536
|
)
|
|
483
537
|
|
|
484
|
-
|
|
538
|
+
self.container.register_singleton(
|
|
539
|
+
UnifiedConfigurationServiceProtocol,
|
|
540
|
+
factory=lambda: self.container.get(UnifiedConfigurationService),
|
|
541
|
+
)
|
|
542
|
+
|
|
485
543
|
from crackerjack.services.config_merge import ConfigMergeService
|
|
486
544
|
|
|
487
545
|
def create_config_merge_service() -> ConfigMergeService:
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
"""File operations with RAII patterns and comprehensive error handling.
|
|
2
|
-
|
|
3
|
-
Provides robust file handling with automatic cleanup, atomic operations,
|
|
4
|
-
and comprehensive error recovery patterns.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
1
|
import asyncio
|
|
8
2
|
import contextlib
|
|
9
3
|
import fcntl
|
|
@@ -19,8 +13,6 @@ from .resource_manager import ResourceManager
|
|
|
19
13
|
|
|
20
14
|
|
|
21
15
|
class AtomicFileWriter(AbstractFileResource):
|
|
22
|
-
"""Atomic file writer with automatic cleanup and rollback on errors."""
|
|
23
|
-
|
|
24
16
|
def __init__(
|
|
25
17
|
self,
|
|
26
18
|
target_path: Path,
|
|
@@ -39,32 +31,24 @@ class AtomicFileWriter(AbstractFileResource):
|
|
|
39
31
|
manager.register_resource(self)
|
|
40
32
|
|
|
41
33
|
async def _do_initialize(self) -> None:
|
|
42
|
-
"""Initialize atomic file writer."""
|
|
43
|
-
# Create temporary file in same directory as target
|
|
44
34
|
self.temp_path = self.path.parent / f".{self.path.name}.tmp.{os.getpid()}"
|
|
45
35
|
|
|
46
|
-
# Create backup if requested and target exists
|
|
47
36
|
if self.backup and self.path.exists():
|
|
48
37
|
self.backup_path = self.path.with_suffix(f"{self.path.suffix}.bak")
|
|
49
38
|
shutil.copy2(self.path, self.backup_path)
|
|
50
39
|
|
|
51
|
-
# Open temporary file for writing
|
|
52
40
|
self._file_handle = self.temp_path.open("w", encoding="utf-8")
|
|
53
41
|
|
|
54
42
|
async def _do_cleanup(self) -> None:
|
|
55
|
-
"""Clean up temporary files and handles."""
|
|
56
|
-
# Close file handle
|
|
57
43
|
if self._file_handle and not self._file_handle.closed:
|
|
58
44
|
self._file_handle.close()
|
|
59
45
|
|
|
60
|
-
# Remove temporary file if it exists
|
|
61
46
|
if self.temp_path and self.temp_path.exists():
|
|
62
47
|
try:
|
|
63
48
|
self.temp_path.unlink()
|
|
64
49
|
except OSError as e:
|
|
65
50
|
self.logger.warning(f"Failed to remove temp file {self.temp_path}: {e}")
|
|
66
51
|
|
|
67
|
-
# Remove backup file if cleanup is successful
|
|
68
52
|
if self.backup_path and self.backup_path.exists():
|
|
69
53
|
try:
|
|
70
54
|
self.backup_path.unlink()
|
|
@@ -74,43 +58,35 @@ class AtomicFileWriter(AbstractFileResource):
|
|
|
74
58
|
)
|
|
75
59
|
|
|
76
60
|
def write(self, content: str) -> None:
|
|
77
|
-
"""Write content to the temporary file."""
|
|
78
61
|
if not self._file_handle:
|
|
79
62
|
raise RuntimeError("AtomicFileWriter not initialized")
|
|
80
63
|
self._file_handle.write(content)
|
|
81
64
|
|
|
82
65
|
def writelines(self, lines: t.Iterable[str]) -> None:
|
|
83
|
-
"""Write multiple lines to the temporary file."""
|
|
84
66
|
if not self._file_handle:
|
|
85
67
|
raise RuntimeError("AtomicFileWriter not initialized")
|
|
86
68
|
self._file_handle.writelines(lines)
|
|
87
69
|
|
|
88
70
|
def flush(self) -> None:
|
|
89
|
-
"""Flush the temporary file."""
|
|
90
71
|
if not self._file_handle:
|
|
91
72
|
raise RuntimeError("AtomicFileWriter not initialized")
|
|
92
73
|
self._file_handle.flush()
|
|
93
74
|
os.fsync(self._file_handle.fileno())
|
|
94
75
|
|
|
95
76
|
async def commit(self) -> None:
|
|
96
|
-
"""Atomically commit the changes to the target file."""
|
|
97
77
|
if not self.temp_path:
|
|
98
78
|
raise RuntimeError("AtomicFileWriter not initialized")
|
|
99
79
|
|
|
100
|
-
# Ensure all data is written
|
|
101
80
|
self.flush()
|
|
102
81
|
|
|
103
|
-
# Close temporary file
|
|
104
82
|
if self._file_handle:
|
|
105
83
|
self._file_handle.close()
|
|
106
84
|
self._file_handle = None
|
|
107
85
|
|
|
108
|
-
# Atomic move
|
|
109
86
|
try:
|
|
110
87
|
self.temp_path.replace(self.path)
|
|
111
88
|
self.logger.debug(f"Successfully committed changes to {self.path}")
|
|
112
89
|
except OSError as e:
|
|
113
|
-
# Restore from backup if available
|
|
114
90
|
if self.backup_path and self.backup_path.exists():
|
|
115
91
|
try:
|
|
116
92
|
self.backup_path.replace(self.path)
|
|
@@ -122,7 +98,6 @@ class AtomicFileWriter(AbstractFileResource):
|
|
|
122
98
|
raise RuntimeError(f"Failed to commit changes to {self.path}") from e
|
|
123
99
|
|
|
124
100
|
async def rollback(self) -> None:
|
|
125
|
-
"""Rollback changes and restore from backup if available."""
|
|
126
101
|
if self.backup_path and self.backup_path.exists():
|
|
127
102
|
try:
|
|
128
103
|
self.backup_path.replace(self.path)
|
|
@@ -133,8 +108,6 @@ class AtomicFileWriter(AbstractFileResource):
|
|
|
133
108
|
|
|
134
109
|
|
|
135
110
|
class LockedFileResource(AbstractFileResource):
|
|
136
|
-
"""File resource with exclusive locking for concurrent access protection."""
|
|
137
|
-
|
|
138
111
|
def __init__(
|
|
139
112
|
self,
|
|
140
113
|
path: Path,
|
|
@@ -152,14 +125,10 @@ class LockedFileResource(AbstractFileResource):
|
|
|
152
125
|
manager.register_resource(self)
|
|
153
126
|
|
|
154
127
|
async def _do_initialize(self) -> None:
|
|
155
|
-
"""Initialize locked file resource."""
|
|
156
|
-
# Ensure parent directory exists
|
|
157
128
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
129
|
|
|
159
|
-
# Open file
|
|
160
130
|
self._file_handle = self.path.open(self.mode)
|
|
161
131
|
|
|
162
|
-
# Acquire exclusive lock with timeout
|
|
163
132
|
start_time = time.time()
|
|
164
133
|
while time.time() - start_time < self.timeout:
|
|
165
134
|
try:
|
|
@@ -174,10 +143,8 @@ class LockedFileResource(AbstractFileResource):
|
|
|
174
143
|
)
|
|
175
144
|
|
|
176
145
|
async def _do_cleanup(self) -> None:
|
|
177
|
-
"""Clean up locked file resource."""
|
|
178
146
|
if self._file_handle and not self._file_handle.closed:
|
|
179
147
|
try:
|
|
180
|
-
# Release lock
|
|
181
148
|
fcntl.flock(self._file_handle.fileno(), fcntl.LOCK_UN)
|
|
182
149
|
self.logger.debug(f"Released lock on {self.path}")
|
|
183
150
|
except OSError as e:
|
|
@@ -187,18 +154,15 @@ class LockedFileResource(AbstractFileResource):
|
|
|
187
154
|
|
|
188
155
|
@property
|
|
189
156
|
def file_handle(self) -> t.IO[str]:
|
|
190
|
-
"""Get the file handle."""
|
|
191
157
|
if not self._file_handle:
|
|
192
158
|
raise RuntimeError("LockedFileResource not initialized")
|
|
193
159
|
return self._file_handle
|
|
194
160
|
|
|
195
161
|
def read(self) -> str:
|
|
196
|
-
"""Read content from the locked file."""
|
|
197
162
|
self.file_handle.seek(0)
|
|
198
163
|
return self.file_handle.read()
|
|
199
164
|
|
|
200
165
|
def write(self, content: str) -> None:
|
|
201
|
-
"""Write content to the locked file."""
|
|
202
166
|
self.file_handle.seek(0)
|
|
203
167
|
self.file_handle.write(content)
|
|
204
168
|
self.file_handle.truncate()
|
|
@@ -207,8 +171,6 @@ class LockedFileResource(AbstractFileResource):
|
|
|
207
171
|
|
|
208
172
|
|
|
209
173
|
class SafeDirectoryCreator(AbstractFileResource):
|
|
210
|
-
"""Safe directory creation with cleanup and rollback capabilities."""
|
|
211
|
-
|
|
212
174
|
def __init__(
|
|
213
175
|
self,
|
|
214
176
|
path: Path,
|
|
@@ -224,18 +186,14 @@ class SafeDirectoryCreator(AbstractFileResource):
|
|
|
224
186
|
manager.register_resource(self)
|
|
225
187
|
|
|
226
188
|
async def _do_initialize(self) -> None:
|
|
227
|
-
"""Initialize safe directory creator."""
|
|
228
|
-
# Track which directories we create
|
|
229
189
|
current = self.path
|
|
230
190
|
|
|
231
191
|
while not current.exists():
|
|
232
192
|
self._created_dirs.append(current)
|
|
233
193
|
current = current.parent
|
|
234
194
|
|
|
235
|
-
# Reverse to create from parent to child
|
|
236
195
|
self._created_dirs.reverse()
|
|
237
196
|
|
|
238
|
-
# Create directories
|
|
239
197
|
for dir_path in self._created_dirs:
|
|
240
198
|
try:
|
|
241
199
|
dir_path.mkdir(exist_ok=True)
|
|
@@ -247,13 +205,10 @@ class SafeDirectoryCreator(AbstractFileResource):
|
|
|
247
205
|
raise
|
|
248
206
|
|
|
249
207
|
async def _do_cleanup(self) -> None:
|
|
250
|
-
"""Clean up created directories if requested."""
|
|
251
208
|
if self.cleanup_on_error:
|
|
252
209
|
await self._cleanup_created_dirs()
|
|
253
210
|
|
|
254
211
|
async def _cleanup_created_dirs(self) -> None:
|
|
255
|
-
"""Remove directories that we created."""
|
|
256
|
-
# Remove in reverse order (child to parent)
|
|
257
212
|
for dir_path in reversed(self._created_dirs):
|
|
258
213
|
try:
|
|
259
214
|
if dir_path.exists() and not any(dir_path.iterdir()):
|
|
@@ -264,8 +219,6 @@ class SafeDirectoryCreator(AbstractFileResource):
|
|
|
264
219
|
|
|
265
220
|
|
|
266
221
|
class BatchFileOperations:
|
|
267
|
-
"""Batch file operations with atomic commit/rollback."""
|
|
268
|
-
|
|
269
222
|
def __init__(self, manager: ResourceManager | None = None) -> None:
|
|
270
223
|
self.manager = manager or ResourceManager()
|
|
271
224
|
self.operations: list[t.Callable[[], None]] = []
|
|
@@ -278,15 +231,13 @@ class BatchFileOperations:
|
|
|
278
231
|
content: str,
|
|
279
232
|
backup: bool = True,
|
|
280
233
|
) -> None:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def write_op():
|
|
234
|
+
def write_op() -> None:
|
|
284
235
|
writer = AtomicFileWriter(path, backup, self.manager)
|
|
285
236
|
asyncio.create_task(writer.initialize())
|
|
286
237
|
writer.write(content)
|
|
287
238
|
asyncio.create_task(writer.commit())
|
|
288
239
|
|
|
289
|
-
def rollback_op():
|
|
240
|
+
def rollback_op() -> None:
|
|
290
241
|
writer = AtomicFileWriter(path, backup)
|
|
291
242
|
asyncio.create_task(writer.rollback())
|
|
292
243
|
|
|
@@ -299,15 +250,13 @@ class BatchFileOperations:
|
|
|
299
250
|
dest: Path,
|
|
300
251
|
backup: bool = True,
|
|
301
252
|
) -> None:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def copy_op():
|
|
253
|
+
def copy_op() -> None:
|
|
305
254
|
if backup and dest.exists():
|
|
306
255
|
backup_path = dest.with_suffix(f"{dest.suffix}.bak")
|
|
307
256
|
shutil.copy2(dest, backup_path)
|
|
308
257
|
shutil.copy2(source, dest)
|
|
309
258
|
|
|
310
|
-
def rollback_op():
|
|
259
|
+
def rollback_op() -> None:
|
|
311
260
|
if backup:
|
|
312
261
|
backup_path = dest.with_suffix(f"{dest.suffix}.bak")
|
|
313
262
|
if backup_path.exists():
|
|
@@ -321,12 +270,10 @@ class BatchFileOperations:
|
|
|
321
270
|
source: Path,
|
|
322
271
|
dest: Path,
|
|
323
272
|
) -> None:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def move_op():
|
|
273
|
+
def move_op() -> None:
|
|
327
274
|
shutil.move(source, dest)
|
|
328
275
|
|
|
329
|
-
def rollback_op():
|
|
276
|
+
def rollback_op() -> None:
|
|
330
277
|
shutil.move(dest, source)
|
|
331
278
|
|
|
332
279
|
self.operations.append(move_op)
|
|
@@ -337,10 +284,9 @@ class BatchFileOperations:
|
|
|
337
284
|
path: Path,
|
|
338
285
|
backup: bool = True,
|
|
339
286
|
) -> None:
|
|
340
|
-
"""Add a delete operation to the batch."""
|
|
341
287
|
backup_path: Path | None = None
|
|
342
288
|
|
|
343
|
-
def delete_op():
|
|
289
|
+
def delete_op() -> None:
|
|
344
290
|
nonlocal backup_path
|
|
345
291
|
if backup and path.exists():
|
|
346
292
|
backup_path = path.with_suffix(f"{path.suffix}.bak.{os.getpid()}")
|
|
@@ -348,7 +294,7 @@ class BatchFileOperations:
|
|
|
348
294
|
elif path.exists():
|
|
349
295
|
path.unlink()
|
|
350
296
|
|
|
351
|
-
def rollback_op():
|
|
297
|
+
def rollback_op() -> None:
|
|
352
298
|
if backup_path and backup_path.exists():
|
|
353
299
|
shutil.move(backup_path, path)
|
|
354
300
|
|
|
@@ -356,7 +302,6 @@ class BatchFileOperations:
|
|
|
356
302
|
self.rollback_operations.append(rollback_op)
|
|
357
303
|
|
|
358
304
|
async def commit_all(self) -> None:
|
|
359
|
-
"""Execute all operations atomically."""
|
|
360
305
|
executed_ops = 0
|
|
361
306
|
|
|
362
307
|
try:
|
|
@@ -369,7 +314,6 @@ class BatchFileOperations:
|
|
|
369
314
|
except Exception as e:
|
|
370
315
|
self.logger.error(f"Batch operation failed at step {executed_ops}: {e}")
|
|
371
316
|
|
|
372
|
-
# Rollback executed operations in reverse order
|
|
373
317
|
for i in range(executed_ops - 1, -1, -1):
|
|
374
318
|
try:
|
|
375
319
|
self.rollback_operations[i]()
|
|
@@ -381,13 +325,11 @@ class BatchFileOperations:
|
|
|
381
325
|
raise RuntimeError("Batch file operations failed and rolled back") from e
|
|
382
326
|
|
|
383
327
|
|
|
384
|
-
# Context managers for common file operations
|
|
385
328
|
@contextlib.asynccontextmanager
|
|
386
329
|
async def atomic_file_write(
|
|
387
330
|
path: Path,
|
|
388
331
|
backup: bool = True,
|
|
389
|
-
):
|
|
390
|
-
"""Context manager for atomic file writing."""
|
|
332
|
+
) -> t.AsyncGenerator[AtomicFileWriter]:
|
|
391
333
|
writer = AtomicFileWriter(path, backup)
|
|
392
334
|
try:
|
|
393
335
|
await writer.initialize()
|
|
@@ -405,8 +347,7 @@ async def locked_file_access(
|
|
|
405
347
|
path: Path,
|
|
406
348
|
mode: str = "r+",
|
|
407
349
|
timeout: float = 30.0,
|
|
408
|
-
):
|
|
409
|
-
"""Context manager for locked file access."""
|
|
350
|
+
) -> t.AsyncGenerator[LockedFileResource]:
|
|
410
351
|
file_resource = LockedFileResource(path, mode, timeout)
|
|
411
352
|
try:
|
|
412
353
|
await file_resource.initialize()
|
|
@@ -419,8 +360,7 @@ async def locked_file_access(
|
|
|
419
360
|
async def safe_directory_creation(
|
|
420
361
|
path: Path,
|
|
421
362
|
cleanup_on_error: bool = True,
|
|
422
|
-
):
|
|
423
|
-
"""Context manager for safe directory creation."""
|
|
363
|
+
) -> t.AsyncGenerator[SafeDirectoryCreator]:
|
|
424
364
|
creator = SafeDirectoryCreator(path, cleanup_on_error)
|
|
425
365
|
try:
|
|
426
366
|
await creator.initialize()
|
|
@@ -430,28 +370,22 @@ async def safe_directory_creation(
|
|
|
430
370
|
|
|
431
371
|
|
|
432
372
|
@contextlib.asynccontextmanager
|
|
433
|
-
async def batch_file_operations():
|
|
434
|
-
"""Context manager for batch file operations."""
|
|
373
|
+
async def batch_file_operations() -> t.AsyncGenerator[BatchFileOperations]:
|
|
435
374
|
batch = BatchFileOperations()
|
|
436
375
|
try:
|
|
437
376
|
yield batch
|
|
438
377
|
await batch.commit_all()
|
|
439
378
|
except Exception:
|
|
440
|
-
# Rollback is handled in commit_all
|
|
441
379
|
raise
|
|
442
380
|
|
|
443
381
|
|
|
444
|
-
# File operation utilities with enhanced error handling
|
|
445
382
|
class SafeFileOperations:
|
|
446
|
-
"""Utility class for safe file operations with comprehensive error handling."""
|
|
447
|
-
|
|
448
383
|
@staticmethod
|
|
449
384
|
async def safe_read_text(
|
|
450
385
|
path: Path,
|
|
451
386
|
encoding: str = "utf-8",
|
|
452
387
|
fallback_encodings: list[str] | None = None,
|
|
453
388
|
) -> str:
|
|
454
|
-
"""Safely read text file with encoding fallback."""
|
|
455
389
|
fallback_encodings = fallback_encodings or ["latin-1", "cp1252"]
|
|
456
390
|
|
|
457
391
|
for enc in [encoding] + fallback_encodings:
|
|
@@ -477,12 +411,10 @@ class SafeFileOperations:
|
|
|
477
411
|
atomic: bool = True,
|
|
478
412
|
backup: bool = True,
|
|
479
413
|
) -> None:
|
|
480
|
-
"""Safely write text file with atomic operation support."""
|
|
481
414
|
if atomic:
|
|
482
415
|
async with atomic_file_write(path, backup) as writer:
|
|
483
416
|
writer.write(content)
|
|
484
417
|
else:
|
|
485
|
-
# Ensure parent directory exists
|
|
486
418
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
487
419
|
path.write_text(content, encoding=encoding)
|
|
488
420
|
|
|
@@ -493,17 +425,14 @@ class SafeFileOperations:
|
|
|
493
425
|
preserve_metadata: bool = True,
|
|
494
426
|
backup: bool = True,
|
|
495
427
|
) -> None:
|
|
496
|
-
"""Safely copy file with backup support."""
|
|
497
428
|
if not source.exists():
|
|
498
429
|
raise FileNotFoundError(f"Source file not found: {source}")
|
|
499
430
|
|
|
500
|
-
# Create backup if requested
|
|
501
431
|
if backup and dest.exists():
|
|
502
432
|
backup_path = dest.with_suffix(f"{dest.suffix}.bak")
|
|
503
433
|
shutil.copy2(dest, backup_path)
|
|
504
434
|
|
|
505
435
|
try:
|
|
506
|
-
# Ensure destination directory exists
|
|
507
436
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
508
437
|
|
|
509
438
|
if preserve_metadata:
|
|
@@ -512,7 +441,6 @@ class SafeFileOperations:
|
|
|
512
441
|
shutil.copy(source, dest)
|
|
513
442
|
|
|
514
443
|
except Exception as e:
|
|
515
|
-
# Restore backup if copy failed
|
|
516
444
|
if backup and dest.with_suffix(f"{dest.suffix}.bak").exists():
|
|
517
445
|
shutil.move(dest.with_suffix(f"{dest.suffix}.bak"), dest)
|
|
518
446
|
raise RuntimeError(f"Failed to copy {source} to {dest}") from e
|
|
@@ -523,27 +451,22 @@ class SafeFileOperations:
|
|
|
523
451
|
dest: Path,
|
|
524
452
|
backup: bool = True,
|
|
525
453
|
) -> None:
|
|
526
|
-
"""Safely move file with backup support."""
|
|
527
454
|
if not source.exists():
|
|
528
455
|
raise FileNotFoundError(f"Source file not found: {source}")
|
|
529
456
|
|
|
530
|
-
# Create backup of destination if it exists
|
|
531
457
|
backup_path = None
|
|
532
458
|
if backup and dest.exists():
|
|
533
459
|
backup_path = dest.with_suffix(f"{dest.suffix}.bak.{os.getpid()}")
|
|
534
460
|
shutil.move(dest, backup_path)
|
|
535
461
|
|
|
536
462
|
try:
|
|
537
|
-
# Ensure destination directory exists
|
|
538
463
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
539
464
|
shutil.move(source, dest)
|
|
540
465
|
|
|
541
|
-
# Remove backup on success
|
|
542
466
|
if backup_path and backup_path.exists():
|
|
543
467
|
backup_path.unlink()
|
|
544
468
|
|
|
545
469
|
except Exception as e:
|
|
546
|
-
# Restore backup if move failed
|
|
547
470
|
if backup_path and backup_path.exists():
|
|
548
471
|
shutil.move(backup_path, dest)
|
|
549
472
|
raise RuntimeError(f"Failed to move {source} to {dest}") from e
|
crackerjack/core/performance.py
CHANGED
|
@@ -142,13 +142,13 @@ class OptimizedFileWatcher:
|
|
|
142
142
|
|
|
143
143
|
@memoize_with_ttl(ttl=30.0)
|
|
144
144
|
def get_python_files(self) -> list[Path]:
|
|
145
|
-
return list(self.root_path.rglob("*.py"))
|
|
145
|
+
return list[t.Any](self.root_path.rglob("*.py"))
|
|
146
146
|
|
|
147
147
|
def get_modified_files(self, since: float) -> list[Path]:
|
|
148
148
|
cache_key = f"modified_since_{since}"
|
|
149
149
|
cached = self._file_cache.get(cache_key)
|
|
150
150
|
if cached is not None:
|
|
151
|
-
return cached
|
|
151
|
+
return t.cast(list[Path], cached)
|
|
152
152
|
modified_files: list[Path] = []
|
|
153
153
|
for py_file in self.get_python_files():
|
|
154
154
|
try:
|