crackerjack 0.31.9__py3-none-any.whl → 0.31.12__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/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +282 -95
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +355 -204
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +52 -62
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +51 -76
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +78 -44
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +281 -433
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.9.dist-info/RECORD +0 -149
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
|
|
3
|
+
from ..services.regex_patterns import SAFE_PATTERNS
|
|
4
4
|
from .base import (
|
|
5
5
|
AgentContext,
|
|
6
6
|
FixResult,
|
|
@@ -15,14 +15,14 @@ class TestSpecialistAgent(SubAgent):
|
|
|
15
15
|
def __init__(self, context: AgentContext) -> None:
|
|
16
16
|
super().__init__(context)
|
|
17
17
|
self.common_test_patterns = {
|
|
18
|
-
"fixture_not_found":
|
|
19
|
-
"import_error":
|
|
20
|
-
"assertion_error":
|
|
21
|
-
"attribute_error":
|
|
22
|
-
"mock_spec_error":
|
|
23
|
-
"hardcoded_path":
|
|
24
|
-
"missing_import":
|
|
25
|
-
"pydantic_validation":
|
|
18
|
+
"fixture_not_found": SAFE_PATTERNS["fixture_not_found_pattern"].pattern,
|
|
19
|
+
"import_error": SAFE_PATTERNS["import_error_pattern"].pattern,
|
|
20
|
+
"assertion_error": SAFE_PATTERNS["assertion_error_pattern"].pattern,
|
|
21
|
+
"attribute_error": SAFE_PATTERNS["attribute_error_pattern"].pattern,
|
|
22
|
+
"mock_spec_error": SAFE_PATTERNS["mock_spec_error_pattern"].pattern,
|
|
23
|
+
"hardcoded_path": SAFE_PATTERNS["hardcoded_path_pattern"].pattern,
|
|
24
|
+
"missing_import": SAFE_PATTERNS["missing_name_pattern"].pattern,
|
|
25
|
+
"pydantic_validation": SAFE_PATTERNS["pydantic_validation_pattern"].pattern,
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
def get_supported_types(self) -> set[IssueType]:
|
|
@@ -63,9 +63,27 @@ class TestSpecialistAgent(SubAgent):
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
def _check_test_patterns(self, message: str) -> float:
|
|
66
|
+
# Map pattern strings back to SAFE_PATTERNS for safe usage
|
|
67
|
+
pattern_map = {
|
|
68
|
+
SAFE_PATTERNS[
|
|
69
|
+
"fixture_not_found_pattern"
|
|
70
|
+
].pattern: "fixture_not_found_pattern",
|
|
71
|
+
SAFE_PATTERNS["import_error_pattern"].pattern: "import_error_pattern",
|
|
72
|
+
SAFE_PATTERNS["assertion_error_pattern"].pattern: "assertion_error_pattern",
|
|
73
|
+
SAFE_PATTERNS["attribute_error_pattern"].pattern: "attribute_error_pattern",
|
|
74
|
+
SAFE_PATTERNS["mock_spec_error_pattern"].pattern: "mock_spec_error_pattern",
|
|
75
|
+
SAFE_PATTERNS["hardcoded_path_pattern"].pattern: "hardcoded_path_pattern",
|
|
76
|
+
SAFE_PATTERNS["missing_name_pattern"].pattern: "missing_name_pattern",
|
|
77
|
+
SAFE_PATTERNS[
|
|
78
|
+
"pydantic_validation_pattern"
|
|
79
|
+
].pattern: "pydantic_validation_pattern",
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
for pattern in self.common_test_patterns.values():
|
|
67
|
-
if
|
|
68
|
-
|
|
83
|
+
if pattern in pattern_map:
|
|
84
|
+
safe_pattern = SAFE_PATTERNS[pattern_map[pattern]]
|
|
85
|
+
if safe_pattern.test(message):
|
|
86
|
+
return 0.9
|
|
69
87
|
return 0.0
|
|
70
88
|
|
|
71
89
|
def _check_test_file_path(self, file_path: str | None) -> float:
|
|
@@ -173,16 +191,40 @@ class TestSpecialistAgent(SubAgent):
|
|
|
173
191
|
def _identify_failure_type(self, issue: Issue) -> str:
|
|
174
192
|
message = issue.message
|
|
175
193
|
|
|
194
|
+
# Map pattern strings back to SAFE_PATTERNS for safe usage
|
|
195
|
+
pattern_map = {
|
|
196
|
+
SAFE_PATTERNS[
|
|
197
|
+
"fixture_not_found_pattern"
|
|
198
|
+
].pattern: "fixture_not_found_pattern",
|
|
199
|
+
SAFE_PATTERNS["import_error_pattern"].pattern: "import_error_pattern",
|
|
200
|
+
SAFE_PATTERNS["assertion_error_pattern"].pattern: "assertion_error_pattern",
|
|
201
|
+
SAFE_PATTERNS["attribute_error_pattern"].pattern: "attribute_error_pattern",
|
|
202
|
+
SAFE_PATTERNS["mock_spec_error_pattern"].pattern: "mock_spec_error_pattern",
|
|
203
|
+
SAFE_PATTERNS["hardcoded_path_pattern"].pattern: "hardcoded_path_pattern",
|
|
204
|
+
SAFE_PATTERNS["missing_name_pattern"].pattern: "missing_name_pattern",
|
|
205
|
+
SAFE_PATTERNS[
|
|
206
|
+
"pydantic_validation_pattern"
|
|
207
|
+
].pattern: "pydantic_validation_pattern",
|
|
208
|
+
}
|
|
209
|
+
|
|
176
210
|
for pattern_name, pattern in self.common_test_patterns.items():
|
|
177
|
-
if
|
|
178
|
-
|
|
211
|
+
if pattern in pattern_map:
|
|
212
|
+
safe_pattern = SAFE_PATTERNS[pattern_map[pattern]]
|
|
213
|
+
if safe_pattern.test(message):
|
|
214
|
+
return pattern_name
|
|
179
215
|
|
|
180
216
|
return "unknown"
|
|
181
217
|
|
|
182
218
|
async def _fix_missing_fixtures(self, issue: Issue) -> list[str]:
|
|
183
219
|
fixes: list[str] = []
|
|
184
220
|
|
|
185
|
-
|
|
221
|
+
# Use safe pattern to test and extract fixture name
|
|
222
|
+
fixture_pattern = SAFE_PATTERNS["fixture_not_found_pattern"]
|
|
223
|
+
if not fixture_pattern.test(issue.message):
|
|
224
|
+
return fixes
|
|
225
|
+
|
|
226
|
+
# Extract fixture name using the safe pattern's search method
|
|
227
|
+
match = fixture_pattern.search(issue.message)
|
|
186
228
|
if not match:
|
|
187
229
|
return fixes
|
|
188
230
|
|
|
@@ -462,7 +504,9 @@ def console() -> Console:
|
|
|
462
504
|
content = "\n".join(lines)
|
|
463
505
|
fixes.append(f"Added pytest import to {file_path}")
|
|
464
506
|
|
|
465
|
-
|
|
507
|
+
from crackerjack.services.regex_patterns import apply_test_fixes
|
|
508
|
+
|
|
509
|
+
content = apply_test_fixes(content)
|
|
466
510
|
|
|
467
511
|
if content != original_content:
|
|
468
512
|
if self.context.write_file_content(path, content):
|
crackerjack/agents/tracker.py
CHANGED
|
@@ -2,7 +2,6 @@ import time
|
|
|
2
2
|
import typing as t
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import Any
|
|
6
5
|
|
|
7
6
|
from .base import FixResult, Issue
|
|
8
7
|
|
|
@@ -42,19 +41,6 @@ class AgentTracker:
|
|
|
42
41
|
"agent_types": agent_types,
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
def track_agent_evaluation(
|
|
46
|
-
self,
|
|
47
|
-
agent_type: str,
|
|
48
|
-
issue: Issue,
|
|
49
|
-
confidence: float,
|
|
50
|
-
) -> None:
|
|
51
|
-
self.active_agents[agent_type] = AgentActivity(
|
|
52
|
-
agent_type=agent_type,
|
|
53
|
-
confidence=confidence,
|
|
54
|
-
status="evaluating",
|
|
55
|
-
current_issue=issue,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
44
|
def track_agent_processing(
|
|
59
45
|
self,
|
|
60
46
|
agent_type: str,
|
|
@@ -89,94 +75,6 @@ class AgentTracker:
|
|
|
89
75
|
self.completed_activities.append(activity)
|
|
90
76
|
del self.active_agents[agent_type]
|
|
91
77
|
|
|
92
|
-
def track_cache_hit(self) -> None:
|
|
93
|
-
self.cache_stats["hits"] += 1
|
|
94
|
-
|
|
95
|
-
def track_cache_miss(self) -> None:
|
|
96
|
-
self.cache_stats["misses"] += 1
|
|
97
|
-
|
|
98
|
-
def get_status(self) -> dict[str, Any]:
|
|
99
|
-
active_agents: list[dict[str, Any]] = []
|
|
100
|
-
|
|
101
|
-
for agent_type, activity in self.active_agents.items():
|
|
102
|
-
agent_data: dict[str, Any] = {
|
|
103
|
-
"agent_type": agent_type,
|
|
104
|
-
"confidence": activity.confidence,
|
|
105
|
-
"status": activity.status,
|
|
106
|
-
"processing_time": time.time() - activity.start_time,
|
|
107
|
-
"start_time": activity.start_time,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if activity.current_issue:
|
|
111
|
-
agent_data["current_issue"] = {
|
|
112
|
-
"type": activity.current_issue.type.value,
|
|
113
|
-
"message": activity.current_issue.message,
|
|
114
|
-
"priority": activity.current_issue.severity.value,
|
|
115
|
-
"file_path": activity.current_issue.file_path,
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
active_agents.append(agent_data)
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
"coordinator_status": self.coordinator_status,
|
|
122
|
-
"active_agents": active_agents,
|
|
123
|
-
"agent_registry": self.agent_registry.copy(),
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
def get_metrics(self) -> dict[str, Any]:
|
|
127
|
-
total_completed = len(self.completed_activities)
|
|
128
|
-
successful = sum(
|
|
129
|
-
1
|
|
130
|
-
for activity in self.completed_activities
|
|
131
|
-
if activity.result and activity.result.success
|
|
132
|
-
)
|
|
133
|
-
success_rate = successful / total_completed if total_completed > 0 else 0.0
|
|
134
|
-
|
|
135
|
-
all_times: list[float] = []
|
|
136
|
-
for times in self.performance_metrics.values():
|
|
137
|
-
all_times.extend(times)
|
|
138
|
-
|
|
139
|
-
avg_processing_time = sum(all_times) / len(all_times) if all_times else 0.0
|
|
140
|
-
|
|
141
|
-
total_requests = self.cache_stats["hits"] + self.cache_stats["misses"]
|
|
142
|
-
cache_hit_rate = (
|
|
143
|
-
self.cache_stats["hits"] / total_requests if total_requests > 0 else 0.0
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
"total_issues_processed": self.total_issues_processed,
|
|
148
|
-
"cache_hits": self.cache_stats["hits"],
|
|
149
|
-
"cache_misses": self.cache_stats["misses"],
|
|
150
|
-
"cache_hit_rate": cache_hit_rate,
|
|
151
|
-
"average_processing_time": avg_processing_time,
|
|
152
|
-
"success_rate": success_rate,
|
|
153
|
-
"completed_activities": total_completed,
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
def get_agent_summary(self) -> dict[str, Any]:
|
|
157
|
-
active_count = len(self.active_agents)
|
|
158
|
-
cache_hits = self.cache_stats["hits"]
|
|
159
|
-
|
|
160
|
-
active_summary: list[dict[str, Any]] = []
|
|
161
|
-
for agent_type, activity in self.active_agents.items():
|
|
162
|
-
emoji = self._get_agent_emoji(agent_type)
|
|
163
|
-
processing_time = time.time() - activity.start_time
|
|
164
|
-
|
|
165
|
-
active_summary.append(
|
|
166
|
-
{
|
|
167
|
-
"display": f"{emoji} {agent_type}: {activity.status.title()} ({processing_time:.1f}s)",
|
|
168
|
-
"agent_type": agent_type,
|
|
169
|
-
"status": activity.status,
|
|
170
|
-
"processing_time": processing_time,
|
|
171
|
-
},
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
"active_count": active_count,
|
|
176
|
-
"cached_fixes": cache_hits,
|
|
177
|
-
"active_agents": active_summary,
|
|
178
|
-
}
|
|
179
|
-
|
|
180
78
|
def _get_agent_emoji(self, agent_type: str) -> str:
|
|
181
79
|
return {
|
|
182
80
|
"FormattingAgent": "🎨",
|
crackerjack/api.py
CHANGED
|
@@ -5,12 +5,13 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from .code_cleaner import CleaningResult, CodeCleaner
|
|
8
|
+
from .code_cleaner import CleaningResult, CodeCleaner, PackageCleaningResult
|
|
9
9
|
from .core.workflow_orchestrator import WorkflowOrchestrator
|
|
10
10
|
from .errors import CrackerjackError, ErrorCode
|
|
11
11
|
from .interactive import InteractiveCLI
|
|
12
12
|
from .interactive import WorkflowOptions as InteractiveWorkflowOptions
|
|
13
13
|
from .models.config import WorkflowOptions
|
|
14
|
+
from .services.regex_patterns import SAFE_PATTERNS
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@dataclass
|
|
@@ -70,7 +71,9 @@ class CrackerjackAPI:
|
|
|
70
71
|
@property
|
|
71
72
|
def code_cleaner(self) -> CodeCleaner:
|
|
72
73
|
if self._code_cleaner is None:
|
|
73
|
-
self._code_cleaner = CodeCleaner(
|
|
74
|
+
self._code_cleaner = CodeCleaner(
|
|
75
|
+
console=self.console, base_directory=self.project_path
|
|
76
|
+
)
|
|
74
77
|
return self._code_cleaner
|
|
75
78
|
|
|
76
79
|
@property
|
|
@@ -126,18 +129,38 @@ class CrackerjackAPI:
|
|
|
126
129
|
self,
|
|
127
130
|
target_dir: Path | None = None,
|
|
128
131
|
backup: bool = True,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
safe_mode: bool = True,
|
|
133
|
+
) -> list[CleaningResult] | PackageCleaningResult:
|
|
134
|
+
"""Clean code with comprehensive backup protection.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
target_dir: Directory to clean (defaults to package root)
|
|
138
|
+
backup: Whether to create backup (deprecated, always True for safety)
|
|
139
|
+
safe_mode: Use comprehensive backup system (default: True, recommended)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
PackageCleaningResult with backup metadata if safe_mode=True,
|
|
143
|
+
otherwise list[CleaningResult] for legacy compatibility
|
|
144
|
+
"""
|
|
145
|
+
target_dir = target_dir or self._get_package_root()
|
|
146
|
+
self.logger.info(f"Cleaning code in {target_dir} (safe_mode={safe_mode})")
|
|
133
147
|
|
|
134
148
|
self._validate_code_before_cleaning(target_dir)
|
|
135
|
-
self._notify_backup_status(backup)
|
|
136
149
|
|
|
137
|
-
|
|
150
|
+
if safe_mode:
|
|
151
|
+
self.console.print(
|
|
152
|
+
"[green]🛡️ Using safe mode with comprehensive backup protection[/green]"
|
|
153
|
+
)
|
|
154
|
+
return self._execute_safe_code_cleaning(target_dir)
|
|
155
|
+
else:
|
|
156
|
+
# Note: Legacy mode still uses backup protection for safety
|
|
157
|
+
self.console.print(
|
|
158
|
+
"[yellow]⚠️ Legacy mode - backup protection still enabled for safety[/yellow]"
|
|
159
|
+
)
|
|
160
|
+
self._notify_backup_status(backup)
|
|
161
|
+
return self._execute_code_cleaning(target_dir)
|
|
138
162
|
|
|
139
163
|
def _validate_code_before_cleaning(self, target_dir: Path) -> None:
|
|
140
|
-
"""Validate code state before cleaning, checking for TODOs."""
|
|
141
164
|
todos_found = self._check_for_todos(target_dir)
|
|
142
165
|
if todos_found:
|
|
143
166
|
self._handle_todos_found(todos_found, target_dir)
|
|
@@ -147,11 +170,10 @@ class CrackerjackAPI:
|
|
|
147
170
|
todos_found: list[tuple[Path, int, str]],
|
|
148
171
|
target_dir: Path,
|
|
149
172
|
) -> None:
|
|
150
|
-
"""Handle case where TODOs are found in codebase."""
|
|
151
173
|
todo_count = len(todos_found)
|
|
152
|
-
self.console.print(f"[red]❌ Found {todo_count} TODO(s) in codebase[/red]")
|
|
174
|
+
self.console.print(f"[red]❌ Found {todo_count} TODO(s) in codebase[/ red]")
|
|
153
175
|
self.console.print(
|
|
154
|
-
"[yellow]Please resolve all TODOs before running code cleaning (
|
|
176
|
+
"[yellow]Please resolve all TODOs before running code cleaning (-x)[/ yellow]",
|
|
155
177
|
)
|
|
156
178
|
|
|
157
179
|
self._display_todo_summary(todos_found, target_dir, todo_count)
|
|
@@ -167,7 +189,6 @@ class CrackerjackAPI:
|
|
|
167
189
|
target_dir: Path,
|
|
168
190
|
todo_count: int,
|
|
169
191
|
) -> None:
|
|
170
|
-
"""Display summary of found TODOs."""
|
|
171
192
|
for _i, (file_path, line_no, content) in enumerate(todos_found[:5]):
|
|
172
193
|
relative_path = file_path.relative_to(target_dir)
|
|
173
194
|
self.console.print(f" {relative_path}: {line_no}: {content.strip()}")
|
|
@@ -176,33 +197,68 @@ class CrackerjackAPI:
|
|
|
176
197
|
self.console.print(f" ... and {todo_count - 5} more")
|
|
177
198
|
|
|
178
199
|
def _notify_backup_status(self, backup: bool) -> None:
|
|
179
|
-
"""Notify user about backup file creation."""
|
|
180
200
|
if backup:
|
|
181
|
-
self.console.print("[yellow]Note: Backup files will be created[/yellow]")
|
|
201
|
+
self.console.print("[yellow]Note: Backup files will be created[/ yellow]")
|
|
202
|
+
|
|
203
|
+
def _execute_safe_code_cleaning(self, target_dir: Path) -> PackageCleaningResult:
|
|
204
|
+
try:
|
|
205
|
+
result = self.code_cleaner.clean_files_with_backup(target_dir)
|
|
206
|
+
self._report_safe_cleaning_results(result)
|
|
207
|
+
return result
|
|
208
|
+
except Exception as e:
|
|
209
|
+
self._handle_cleaning_error(e)
|
|
182
210
|
|
|
183
211
|
def _execute_code_cleaning(self, target_dir: Path) -> list[CleaningResult]:
|
|
184
|
-
"""Execute code cleaning and handle results."""
|
|
185
212
|
try:
|
|
186
|
-
|
|
187
|
-
self.
|
|
213
|
+
# Use backup protection by default for safety
|
|
214
|
+
results = self.code_cleaner.clean_files(target_dir, use_backup=True)
|
|
215
|
+
|
|
216
|
+
# Handle both return types (legacy compatibility)
|
|
217
|
+
if isinstance(results, list):
|
|
218
|
+
self._report_cleaning_results(results)
|
|
219
|
+
else:
|
|
220
|
+
# PackageCleaningResult from backup mode
|
|
221
|
+
self._report_safe_cleaning_results(results)
|
|
222
|
+
results = results.file_results # Extract list for compatibility
|
|
223
|
+
|
|
188
224
|
return results
|
|
189
225
|
except Exception as e:
|
|
190
226
|
self._handle_cleaning_error(e)
|
|
191
227
|
|
|
228
|
+
def _report_safe_cleaning_results(self, result: PackageCleaningResult) -> None:
|
|
229
|
+
if result.overall_success:
|
|
230
|
+
self.console.print(
|
|
231
|
+
f"[green]🎉 Package cleaning completed successfully![/green] "
|
|
232
|
+
f"({result.successful_files}/{result.total_files} files cleaned)"
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
self.console.print(
|
|
236
|
+
f"[red]❌ Package cleaning failed![/red] "
|
|
237
|
+
f"({result.failed_files}/{result.total_files} files failed)"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if result.backup_restored:
|
|
241
|
+
self.console.print(
|
|
242
|
+
"[yellow]⚠️ Files were automatically restored from backup[/yellow]"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if result.backup_metadata:
|
|
246
|
+
self.console.print(
|
|
247
|
+
f"[blue]📦 Backup available at: {result.backup_metadata.backup_directory}[/blue]"
|
|
248
|
+
)
|
|
249
|
+
|
|
192
250
|
def _report_cleaning_results(self, results: list[CleaningResult]) -> None:
|
|
193
|
-
"""Report cleaning results to user."""
|
|
194
251
|
successful = sum(1 for r in results if r.success)
|
|
195
252
|
failed = len(results) - successful
|
|
196
253
|
|
|
197
254
|
if successful > 0:
|
|
198
255
|
self.console.print(
|
|
199
|
-
f"[green]✅ Successfully cleaned {successful} files[/green]",
|
|
256
|
+
f"[green]✅ Successfully cleaned {successful} files[/ green]",
|
|
200
257
|
)
|
|
201
258
|
if failed > 0:
|
|
202
|
-
self.console.print(f"[red]❌ Failed to clean {failed} files[/red]")
|
|
259
|
+
self.console.print(f"[red]❌ Failed to clean {failed} files[/ red]")
|
|
203
260
|
|
|
204
261
|
def _handle_cleaning_error(self, error: Exception) -> t.NoReturn:
|
|
205
|
-
"""Handle code cleaning errors."""
|
|
206
262
|
self.logger.error(f"Code cleaning failed: {error}")
|
|
207
263
|
raise CrackerjackError(
|
|
208
264
|
message=f"Code cleaning failed: {error}",
|
|
@@ -263,7 +319,7 @@ class CrackerjackAPI:
|
|
|
263
319
|
) -> PublishResult:
|
|
264
320
|
try:
|
|
265
321
|
self.logger.info(
|
|
266
|
-
f"Publishing package (version_bump
|
|
322
|
+
f"Publishing package (version_bump={version_bump}, dry_run={dry_run})",
|
|
267
323
|
)
|
|
268
324
|
|
|
269
325
|
options = self._create_options(
|
|
@@ -304,7 +360,7 @@ class CrackerjackAPI:
|
|
|
304
360
|
return self.interactive_cli.run_interactive_workflow(options)
|
|
305
361
|
except Exception as e:
|
|
306
362
|
self.logger.exception(f"Interactive workflow failed: {e}")
|
|
307
|
-
self.console.print(f"[red]❌ Interactive workflow failed: {e}[/red]")
|
|
363
|
+
self.console.print(f"[red]❌ Interactive workflow failed: {e}[/ red]")
|
|
308
364
|
return False
|
|
309
365
|
|
|
310
366
|
def create_workflow_options(
|
|
@@ -371,7 +427,7 @@ class CrackerjackAPI:
|
|
|
371
427
|
"has_requirements_txt": (
|
|
372
428
|
self.project_path / "requirements.txt"
|
|
373
429
|
).exists(),
|
|
374
|
-
"has_tests": any(self.project_path.rglob("test*.py")),
|
|
430
|
+
"has_tests": any(self.project_path.rglob("test *.py")),
|
|
375
431
|
}
|
|
376
432
|
|
|
377
433
|
except Exception as e:
|
|
@@ -458,15 +514,12 @@ class CrackerjackAPI:
|
|
|
458
514
|
return "unknown"
|
|
459
515
|
|
|
460
516
|
def _check_for_todos(self, target_dir: Path) -> list[tuple[Path, int, str]]:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
task_pattern = re.compile(f"#.*?{'T'}{'O'}{'D'}{'O'}.*", re.IGNORECASE)
|
|
517
|
+
# Use SAFE_PATTERNS for TODO detection
|
|
518
|
+
task_pattern = SAFE_PATTERNS["todo_pattern"]
|
|
465
519
|
python_files = self._get_python_files_for_todo_check(target_dir)
|
|
466
520
|
return self._scan_files_for_todos(python_files, task_pattern)
|
|
467
521
|
|
|
468
522
|
def _get_python_files_for_todo_check(self, target_dir: Path) -> list[Path]:
|
|
469
|
-
"""Get list of Python files to check for TODOs, excluding ignored directories."""
|
|
470
523
|
python_files: list[Path] = []
|
|
471
524
|
ignore_patterns = self._get_ignore_patterns()
|
|
472
525
|
|
|
@@ -477,7 +530,6 @@ class CrackerjackAPI:
|
|
|
477
530
|
return python_files
|
|
478
531
|
|
|
479
532
|
def _get_ignore_patterns(self) -> set[str]:
|
|
480
|
-
"""Get patterns for directories/files to ignore during TODO scanning."""
|
|
481
533
|
return {
|
|
482
534
|
"__pycache__",
|
|
483
535
|
".git",
|
|
@@ -486,10 +538,13 @@ class CrackerjackAPI:
|
|
|
486
538
|
".pytest_cache",
|
|
487
539
|
"build",
|
|
488
540
|
"dist",
|
|
541
|
+
"tests",
|
|
542
|
+
"test",
|
|
543
|
+
"examples",
|
|
544
|
+
"example",
|
|
489
545
|
}
|
|
490
546
|
|
|
491
547
|
def _should_skip_file(self, py_file: Path, ignore_patterns: set[str]) -> bool:
|
|
492
|
-
"""Check if a Python file should be skipped during TODO scanning."""
|
|
493
548
|
if py_file.name.startswith("."):
|
|
494
549
|
return True
|
|
495
550
|
|
|
@@ -500,7 +555,6 @@ class CrackerjackAPI:
|
|
|
500
555
|
python_files: list[Path],
|
|
501
556
|
todo_pattern: t.Any,
|
|
502
557
|
) -> list[tuple[Path, int, str]]:
|
|
503
|
-
"""Scan Python files for TODO comments."""
|
|
504
558
|
todos_found: list[tuple[Path, int, str]] = []
|
|
505
559
|
|
|
506
560
|
for file_path in python_files:
|
|
@@ -514,18 +568,69 @@ class CrackerjackAPI:
|
|
|
514
568
|
file_path: Path,
|
|
515
569
|
todo_pattern: t.Any,
|
|
516
570
|
) -> list[tuple[Path, int, str]]:
|
|
517
|
-
"""Scan a single file for TODO comments."""
|
|
518
571
|
todos: list[tuple[Path, int, str]] = []
|
|
519
572
|
from contextlib import suppress
|
|
520
573
|
|
|
521
574
|
with suppress(UnicodeDecodeError, PermissionError):
|
|
522
575
|
with file_path.open() as f:
|
|
523
576
|
for line_no, line in enumerate(f, 1):
|
|
524
|
-
if
|
|
577
|
+
# For ValidatedPattern, check if applying it changes the line
|
|
578
|
+
# If it doesn't change, then it didn't match (identity replacement for match-only patterns)
|
|
579
|
+
original = line.strip()
|
|
580
|
+
processed = todo_pattern.apply(original)
|
|
581
|
+
# For TODO pattern with identity replacement, a match means no change
|
|
582
|
+
# But we need to check if it actually contains TODO
|
|
583
|
+
if "todo" in original.lower() and original == processed:
|
|
525
584
|
todos.append((file_path, line_no, line))
|
|
526
585
|
|
|
527
586
|
return todos
|
|
528
587
|
|
|
588
|
+
def _get_package_root(self) -> Path:
|
|
589
|
+
package_name = self._read_package_name_from_pyproject()
|
|
590
|
+
if package_name:
|
|
591
|
+
package_dir = self._find_package_directory_by_name(package_name)
|
|
592
|
+
if package_dir:
|
|
593
|
+
return package_dir
|
|
594
|
+
|
|
595
|
+
fallback_dir = self._find_fallback_package_directory()
|
|
596
|
+
return fallback_dir or self.project_path
|
|
597
|
+
|
|
598
|
+
def _read_package_name_from_pyproject(self) -> str | None:
|
|
599
|
+
pyproject_path = self.project_path / "pyproject.toml"
|
|
600
|
+
if not pyproject_path.exists():
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
from contextlib import suppress
|
|
604
|
+
|
|
605
|
+
with suppress(Exception):
|
|
606
|
+
import tomllib
|
|
607
|
+
|
|
608
|
+
with pyproject_path.open("rb") as f:
|
|
609
|
+
data = tomllib.load(f)
|
|
610
|
+
|
|
611
|
+
if "project" in data and "name" in data["project"]:
|
|
612
|
+
return data["project"]["name"]
|
|
613
|
+
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
def _find_package_directory_by_name(self, package_name: str) -> Path | None:
|
|
617
|
+
package_dir = self.project_path / package_name
|
|
618
|
+
if package_dir.exists() and package_dir.is_dir():
|
|
619
|
+
return package_dir
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
def _find_fallback_package_directory(self) -> Path | None:
|
|
623
|
+
for possible_name in ("src", self.project_path.name):
|
|
624
|
+
package_dir = self.project_path / possible_name
|
|
625
|
+
if self._is_valid_python_package_directory(package_dir):
|
|
626
|
+
return package_dir
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
def _is_valid_python_package_directory(self, directory: Path) -> bool:
|
|
630
|
+
if not (directory.exists() and directory.is_dir()):
|
|
631
|
+
return False
|
|
632
|
+
return any(directory.glob("*.py"))
|
|
633
|
+
|
|
529
634
|
|
|
530
635
|
def run_quality_checks(
|
|
531
636
|
project_path: Path | None = None,
|
|
@@ -541,8 +646,11 @@ def run_quality_checks(
|
|
|
541
646
|
def clean_code(
|
|
542
647
|
project_path: Path | None = None,
|
|
543
648
|
backup: bool = True,
|
|
544
|
-
|
|
545
|
-
|
|
649
|
+
safe_mode: bool = True,
|
|
650
|
+
) -> list[CleaningResult] | PackageCleaningResult:
|
|
651
|
+
return CrackerjackAPI(project_path=project_path).clean_code(
|
|
652
|
+
backup=backup, safe_mode=safe_mode
|
|
653
|
+
)
|
|
546
654
|
|
|
547
655
|
|
|
548
656
|
def run_tests(project_path: Path | None = None, coverage: bool = False) -> TestResult:
|