crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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 +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -253
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +670 -0
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +577 -0
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config-ai.yaml +0 -149
- crackerjack/.pre-commit-config-fast.yaml +0 -69
- crackerjack/.pre-commit-config.yaml +0 -114
- crackerjack/crackerjack.py +0 -4140
- crackerjack/pyproject.toml +0 -285
- crackerjack-0.29.0.dist-info/METADATA +0 -1289
- crackerjack-0.29.0.dist-info/RECORD +0 -17
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import typing as t
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from .errors import ErrorCode, ExecutionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CleaningStepResult(Enum):
|
|
15
|
+
SUCCESS = "success"
|
|
16
|
+
FAILED = "failed"
|
|
17
|
+
SKIPPED = "skipped"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CleaningResult:
|
|
22
|
+
file_path: Path
|
|
23
|
+
success: bool
|
|
24
|
+
steps_completed: list[str]
|
|
25
|
+
steps_failed: list[str]
|
|
26
|
+
warnings: list[str]
|
|
27
|
+
original_size: int
|
|
28
|
+
cleaned_size: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileProcessorProtocol(Protocol):
|
|
32
|
+
def read_file_safely(self, file_path: Path) -> str: ...
|
|
33
|
+
def write_file_safely(self, file_path: Path, content: str) -> None: ...
|
|
34
|
+
def backup_file(self, file_path: Path) -> Path: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CleaningStepProtocol(Protocol):
|
|
38
|
+
def __call__(self, code: str, file_path: Path) -> str: ...
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def name(self) -> str: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ErrorHandlerProtocol(Protocol):
|
|
45
|
+
def handle_file_error(
|
|
46
|
+
self,
|
|
47
|
+
file_path: Path,
|
|
48
|
+
error: Exception,
|
|
49
|
+
step: str,
|
|
50
|
+
) -> None: ...
|
|
51
|
+
def log_cleaning_result(self, result: CleaningResult) -> None: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FileProcessor(BaseModel):
|
|
55
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
56
|
+
|
|
57
|
+
console: Console
|
|
58
|
+
logger: t.Any = None
|
|
59
|
+
|
|
60
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
61
|
+
if self.logger is None:
|
|
62
|
+
import logging
|
|
63
|
+
|
|
64
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner.file_processor")
|
|
65
|
+
|
|
66
|
+
def read_file_safely(self, file_path: Path) -> str:
|
|
67
|
+
try:
|
|
68
|
+
return file_path.read_text(encoding="utf - 8")
|
|
69
|
+
except UnicodeDecodeError:
|
|
70
|
+
for encoding in ("latin1", "cp1252"):
|
|
71
|
+
try:
|
|
72
|
+
content = file_path.read_text(encoding=encoding)
|
|
73
|
+
self.logger.warning(
|
|
74
|
+
f"File {file_path} read with {encoding} encoding",
|
|
75
|
+
)
|
|
76
|
+
return content
|
|
77
|
+
except UnicodeDecodeError:
|
|
78
|
+
continue
|
|
79
|
+
raise ExecutionError(
|
|
80
|
+
message=f"Could not decode file {file_path}",
|
|
81
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
82
|
+
)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise ExecutionError(
|
|
85
|
+
message=f"Failed to read file {file_path}: {e}",
|
|
86
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
87
|
+
) from e
|
|
88
|
+
|
|
89
|
+
def write_file_safely(self, file_path: Path, content: str) -> None:
|
|
90
|
+
try:
|
|
91
|
+
file_path.write_text(content, encoding="utf - 8")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ExecutionError(
|
|
94
|
+
message=f"Failed to write file {file_path}: {e}",
|
|
95
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
96
|
+
) from e
|
|
97
|
+
|
|
98
|
+
def backup_file(self, file_path: Path) -> Path:
|
|
99
|
+
backup_path = file_path.with_suffix(f"{file_path.suffix}.backup")
|
|
100
|
+
try:
|
|
101
|
+
backup_path.write_bytes(file_path.read_bytes())
|
|
102
|
+
return backup_path
|
|
103
|
+
except Exception as e:
|
|
104
|
+
raise ExecutionError(
|
|
105
|
+
message=f"Failed to create backup for {file_path}: {e}",
|
|
106
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
107
|
+
) from e
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CleaningErrorHandler(BaseModel):
|
|
111
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
112
|
+
|
|
113
|
+
console: Console
|
|
114
|
+
logger: t.Any = None
|
|
115
|
+
|
|
116
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
117
|
+
if self.logger is None:
|
|
118
|
+
import logging
|
|
119
|
+
|
|
120
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner.error_handler")
|
|
121
|
+
|
|
122
|
+
def handle_file_error(self, file_path: Path, error: Exception, step: str) -> None:
|
|
123
|
+
self.console.print(
|
|
124
|
+
f"[bold bright_yellow]⚠️ Warning: {step} failed for {file_path}: {error}[/bold bright_yellow]",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.logger.warning(
|
|
128
|
+
"Cleaning step failed",
|
|
129
|
+
extra={
|
|
130
|
+
"file_path": str(file_path),
|
|
131
|
+
"step": step,
|
|
132
|
+
"error": str(error),
|
|
133
|
+
"error_type": type(error).__name__,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def log_cleaning_result(self, result: CleaningResult) -> None:
|
|
138
|
+
if result.success:
|
|
139
|
+
self.console.print(
|
|
140
|
+
f"[green]✅ Cleaned {result.file_path}[/green] "
|
|
141
|
+
f"({result.original_size} → {result.cleaned_size} bytes)",
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
self.console.print(
|
|
145
|
+
f"[red]❌ Failed to clean {result.file_path}[/red] "
|
|
146
|
+
f"({len(result.steps_failed)} steps failed)",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if result.warnings:
|
|
150
|
+
for warning in result.warnings:
|
|
151
|
+
self.console.print(f"[yellow]⚠️ {warning}[/yellow]")
|
|
152
|
+
|
|
153
|
+
self.logger.info(
|
|
154
|
+
"File cleaning completed",
|
|
155
|
+
extra={
|
|
156
|
+
"file_path": str(result.file_path),
|
|
157
|
+
"success": result.success,
|
|
158
|
+
"steps_completed": result.steps_completed,
|
|
159
|
+
"steps_failed": result.steps_failed,
|
|
160
|
+
"original_size": result.original_size,
|
|
161
|
+
"cleaned_size": result.cleaned_size,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class CleaningPipeline(BaseModel):
|
|
167
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
168
|
+
|
|
169
|
+
file_processor: t.Any
|
|
170
|
+
error_handler: t.Any
|
|
171
|
+
console: Console
|
|
172
|
+
logger: t.Any = None
|
|
173
|
+
|
|
174
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
175
|
+
if self.logger is None:
|
|
176
|
+
import logging
|
|
177
|
+
|
|
178
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner.pipeline")
|
|
179
|
+
|
|
180
|
+
def clean_file(
|
|
181
|
+
self,
|
|
182
|
+
file_path: Path,
|
|
183
|
+
cleaning_steps: list[CleaningStepProtocol],
|
|
184
|
+
) -> CleaningResult:
|
|
185
|
+
self.logger.info(f"Starting clean_file for {file_path}")
|
|
186
|
+
try:
|
|
187
|
+
original_code = self.file_processor.read_file_safely(file_path)
|
|
188
|
+
original_size = len(original_code.encode("utf - 8"))
|
|
189
|
+
|
|
190
|
+
result = self._apply_cleaning_pipeline(
|
|
191
|
+
original_code,
|
|
192
|
+
file_path,
|
|
193
|
+
cleaning_steps,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if result.success and result.cleaned_code != original_code:
|
|
197
|
+
self.file_processor.write_file_safely(file_path, result.cleaned_code)
|
|
198
|
+
cleaned_size = len(result.cleaned_code.encode("utf - 8"))
|
|
199
|
+
else:
|
|
200
|
+
cleaned_size = original_size
|
|
201
|
+
|
|
202
|
+
cleaning_result = CleaningResult(
|
|
203
|
+
file_path=file_path,
|
|
204
|
+
success=result.success,
|
|
205
|
+
steps_completed=result.steps_completed,
|
|
206
|
+
steps_failed=result.steps_failed,
|
|
207
|
+
warnings=result.warnings,
|
|
208
|
+
original_size=original_size,
|
|
209
|
+
cleaned_size=cleaned_size,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self.error_handler.log_cleaning_result(cleaning_result)
|
|
213
|
+
return cleaning_result
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self.error_handler.handle_file_error(file_path, e, "file_processing")
|
|
217
|
+
return CleaningResult(
|
|
218
|
+
file_path=file_path,
|
|
219
|
+
success=False,
|
|
220
|
+
steps_completed=[],
|
|
221
|
+
steps_failed=["file_processing"],
|
|
222
|
+
warnings=[],
|
|
223
|
+
original_size=0,
|
|
224
|
+
cleaned_size=0,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class PipelineResult:
|
|
229
|
+
cleaned_code: str
|
|
230
|
+
success: bool
|
|
231
|
+
steps_completed: list[str]
|
|
232
|
+
steps_failed: list[str]
|
|
233
|
+
warnings: list[str]
|
|
234
|
+
|
|
235
|
+
def _apply_cleaning_pipeline(
|
|
236
|
+
self,
|
|
237
|
+
code: str,
|
|
238
|
+
file_path: Path,
|
|
239
|
+
cleaning_steps: list[CleaningStepProtocol],
|
|
240
|
+
) -> PipelineResult:
|
|
241
|
+
current_code = code
|
|
242
|
+
steps_completed: list[str] = []
|
|
243
|
+
steps_failed: list[str] = []
|
|
244
|
+
warnings: list[str] = []
|
|
245
|
+
overall_success = True
|
|
246
|
+
|
|
247
|
+
for step in cleaning_steps:
|
|
248
|
+
try:
|
|
249
|
+
step_result = step(current_code, file_path)
|
|
250
|
+
current_code = step_result
|
|
251
|
+
steps_completed.append(step.name)
|
|
252
|
+
|
|
253
|
+
self.logger.debug(
|
|
254
|
+
"Cleaning step completed",
|
|
255
|
+
extra={"step": step.name, "file_path": str(file_path)},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
self.error_handler.handle_file_error(file_path, e, step.name)
|
|
260
|
+
steps_failed.append(step.name)
|
|
261
|
+
warnings.append(f"{step.name} failed: {e}")
|
|
262
|
+
|
|
263
|
+
self.logger.warning(
|
|
264
|
+
"Cleaning step failed, continuing with original code",
|
|
265
|
+
extra={
|
|
266
|
+
"step": step.name,
|
|
267
|
+
"file_path": str(file_path),
|
|
268
|
+
"error": str(e),
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if steps_failed:
|
|
273
|
+
success_ratio = len(steps_completed) / (
|
|
274
|
+
len(steps_completed) + len(steps_failed)
|
|
275
|
+
)
|
|
276
|
+
overall_success = success_ratio >= 0.7
|
|
277
|
+
|
|
278
|
+
return self.PipelineResult(
|
|
279
|
+
cleaned_code=current_code,
|
|
280
|
+
success=overall_success,
|
|
281
|
+
steps_completed=steps_completed,
|
|
282
|
+
steps_failed=steps_failed,
|
|
283
|
+
warnings=warnings,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class CodeCleaner(BaseModel):
|
|
288
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
289
|
+
|
|
290
|
+
console: Console
|
|
291
|
+
file_processor: t.Any = None
|
|
292
|
+
error_handler: t.Any = None
|
|
293
|
+
pipeline: t.Any = None
|
|
294
|
+
logger: t.Any = None
|
|
295
|
+
|
|
296
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
297
|
+
if self.logger is None:
|
|
298
|
+
import logging
|
|
299
|
+
|
|
300
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner")
|
|
301
|
+
|
|
302
|
+
if self.file_processor is None:
|
|
303
|
+
self.file_processor = FileProcessor(console=self.console)
|
|
304
|
+
|
|
305
|
+
if self.error_handler is None:
|
|
306
|
+
self.error_handler = CleaningErrorHandler(console=self.console)
|
|
307
|
+
|
|
308
|
+
if self.pipeline is None:
|
|
309
|
+
self.pipeline = CleaningPipeline(
|
|
310
|
+
file_processor=self.file_processor,
|
|
311
|
+
error_handler=self.error_handler,
|
|
312
|
+
console=self.console,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def clean_file(self, file_path: Path) -> CleaningResult:
|
|
316
|
+
cleaning_steps = [
|
|
317
|
+
self._create_line_comment_step(),
|
|
318
|
+
self._create_docstring_step(),
|
|
319
|
+
self._create_whitespace_step(),
|
|
320
|
+
self._create_formatting_step(),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
return self.pipeline.clean_file(file_path, cleaning_steps)
|
|
324
|
+
|
|
325
|
+
def clean_files(self, pkg_dir: Path | None = None) -> list[CleaningResult]:
|
|
326
|
+
if pkg_dir is None:
|
|
327
|
+
pkg_dir = Path.cwd()
|
|
328
|
+
|
|
329
|
+
python_files = list(pkg_dir.rglob(" * .py"))
|
|
330
|
+
results: list[CleaningResult] = []
|
|
331
|
+
|
|
332
|
+
self.logger.info(f"Starting clean_files for {len(python_files)} files")
|
|
333
|
+
for file_path in python_files:
|
|
334
|
+
if self.should_process_file(file_path):
|
|
335
|
+
result = self.clean_file(file_path)
|
|
336
|
+
results.append(result)
|
|
337
|
+
|
|
338
|
+
return results
|
|
339
|
+
|
|
340
|
+
def should_process_file(self, file_path: Path) -> bool:
|
|
341
|
+
ignore_patterns = {
|
|
342
|
+
"__pycache__",
|
|
343
|
+
".git",
|
|
344
|
+
".venv",
|
|
345
|
+
"site - packages",
|
|
346
|
+
".pytest_cache",
|
|
347
|
+
"build",
|
|
348
|
+
"dist",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for parent in file_path.parents:
|
|
352
|
+
if parent.name in ignore_patterns:
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
return not (file_path.name.startswith(".") or file_path.suffix != ".py")
|
|
356
|
+
|
|
357
|
+
def _create_line_comment_step(self) -> CleaningStepProtocol:
|
|
358
|
+
"""Create a step for removing line comments while preserving special comments."""
|
|
359
|
+
return self._LineCommentStep()
|
|
360
|
+
|
|
361
|
+
def _create_docstring_step(self) -> CleaningStepProtocol:
|
|
362
|
+
"""Create a step for removing docstrings."""
|
|
363
|
+
return self._DocstringStep()
|
|
364
|
+
|
|
365
|
+
class _DocstringStep:
|
|
366
|
+
"""Step implementation for removing docstrings."""
|
|
367
|
+
|
|
368
|
+
name = "remove_docstrings"
|
|
369
|
+
|
|
370
|
+
def _is_docstring_node(self, node: ast.AST) -> bool:
|
|
371
|
+
body = getattr(node, "body", None)
|
|
372
|
+
return (
|
|
373
|
+
hasattr(node, "body")
|
|
374
|
+
and body is not None
|
|
375
|
+
and len(body) > 0
|
|
376
|
+
and isinstance(body[0], ast.Expr)
|
|
377
|
+
and isinstance(body[0].value, ast.Constant)
|
|
378
|
+
and isinstance(body[0].value.value, str)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def _find_docstrings(self, tree: ast.AST) -> list[ast.AST]:
|
|
382
|
+
docstring_nodes: list[ast.AST] = []
|
|
383
|
+
finder = self._DocstringFinder(docstring_nodes, self._is_docstring_node)
|
|
384
|
+
finder.visit(tree)
|
|
385
|
+
return docstring_nodes
|
|
386
|
+
|
|
387
|
+
class _DocstringFinder(ast.NodeVisitor):
|
|
388
|
+
def __init__(
|
|
389
|
+
self,
|
|
390
|
+
docstring_nodes: list[ast.AST],
|
|
391
|
+
is_docstring_node: t.Callable[[ast.AST], bool],
|
|
392
|
+
):
|
|
393
|
+
self.docstring_nodes = docstring_nodes
|
|
394
|
+
self.is_docstring_node = is_docstring_node
|
|
395
|
+
|
|
396
|
+
def _add_if_docstring(self, node: ast.AST) -> None:
|
|
397
|
+
if self.is_docstring_node(node) and hasattr(node, "body"):
|
|
398
|
+
body: list[ast.stmt] = getattr(node, "body")
|
|
399
|
+
self.docstring_nodes.append(body[0])
|
|
400
|
+
self.generic_visit(node)
|
|
401
|
+
|
|
402
|
+
def visit_Module(self, node: ast.Module) -> None:
|
|
403
|
+
self._add_if_docstring(node)
|
|
404
|
+
|
|
405
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
406
|
+
self._add_if_docstring(node)
|
|
407
|
+
|
|
408
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
409
|
+
self._add_if_docstring(node)
|
|
410
|
+
|
|
411
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
412
|
+
self._add_if_docstring(node)
|
|
413
|
+
|
|
414
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
415
|
+
try:
|
|
416
|
+
tree = ast.parse(code, filename=str(file_path))
|
|
417
|
+
except SyntaxError:
|
|
418
|
+
return self._regex_fallback_removal(code)
|
|
419
|
+
|
|
420
|
+
docstring_nodes = self._find_docstrings(tree)
|
|
421
|
+
|
|
422
|
+
if not docstring_nodes:
|
|
423
|
+
return code
|
|
424
|
+
|
|
425
|
+
lines = code.split("\n")
|
|
426
|
+
lines_to_remove: set[int] = set()
|
|
427
|
+
|
|
428
|
+
for node in docstring_nodes:
|
|
429
|
+
# Most AST nodes have lineno and end_lineno attributes
|
|
430
|
+
start_line = getattr(node, "lineno", 1)
|
|
431
|
+
end_line = getattr(node, "end_lineno", start_line + 1)
|
|
432
|
+
lines_to_remove.update(range(start_line, end_line))
|
|
433
|
+
|
|
434
|
+
result_lines = [
|
|
435
|
+
line for i, line in enumerate(lines, 1) if i not in lines_to_remove
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
result = "\n".join(result_lines)
|
|
439
|
+
return self._regex_fallback_removal(result)
|
|
440
|
+
|
|
441
|
+
def _regex_fallback_removal(self, code: str) -> str:
|
|
442
|
+
import re
|
|
443
|
+
|
|
444
|
+
patterns = [
|
|
445
|
+
r'^\s*""".*?"""\s*$',
|
|
446
|
+
r"^\s*'''.*?'''\s*$",
|
|
447
|
+
r'^\s*""".*?"""\s*$',
|
|
448
|
+
r"^\s*'''.*?'''\s*$",
|
|
449
|
+
]
|
|
450
|
+
result = code
|
|
451
|
+
for pattern in patterns:
|
|
452
|
+
result = re.sub(pattern, "", result, flags=re.MULTILINE | re.DOTALL)
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
class _LineCommentStep:
|
|
456
|
+
"""Step implementation for removing line comments."""
|
|
457
|
+
|
|
458
|
+
name = "remove_line_comments"
|
|
459
|
+
|
|
460
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
461
|
+
lines = code.split("\n")
|
|
462
|
+
# Performance: Use list comprehension instead of generator for small-to-medium files
|
|
463
|
+
processed_lines = [self._process_line_for_comments(line) for line in lines]
|
|
464
|
+
return "\n".join(processed_lines)
|
|
465
|
+
|
|
466
|
+
def _process_line_for_comments(self, line: str) -> str:
|
|
467
|
+
"""Process a single line to remove comments while preserving strings."""
|
|
468
|
+
if not line.strip() or self._is_preserved_comment_line(line):
|
|
469
|
+
return line
|
|
470
|
+
return self._remove_comment_from_line(line)
|
|
471
|
+
|
|
472
|
+
def _is_preserved_comment_line(self, line: str) -> bool:
|
|
473
|
+
"""Check if this comment line should be preserved."""
|
|
474
|
+
stripped = line.strip()
|
|
475
|
+
if not stripped.startswith("#"):
|
|
476
|
+
return False
|
|
477
|
+
return self._has_preserved_pattern(stripped)
|
|
478
|
+
|
|
479
|
+
def _has_preserved_pattern(self, stripped_line: str) -> bool:
|
|
480
|
+
"""Check if line contains preserved comment patterns."""
|
|
481
|
+
preserved_patterns = ["coding: ", "encoding: ", "type: ", "noqa", "pragma"]
|
|
482
|
+
return stripped_line.startswith("# !/ ") or any(
|
|
483
|
+
pattern in stripped_line for pattern in preserved_patterns
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
def _remove_comment_from_line(self, line: str) -> str:
|
|
487
|
+
"""Remove comments from a line while preserving string literals."""
|
|
488
|
+
result: list[str] = []
|
|
489
|
+
string_state: dict[str, t.Any] = {"in_string": False, "quote_char": None}
|
|
490
|
+
for i, char in enumerate(line):
|
|
491
|
+
if self._should_break_at_comment(char, string_state):
|
|
492
|
+
break
|
|
493
|
+
self._update_string_state(char, i, line, string_state)
|
|
494
|
+
result.append(char)
|
|
495
|
+
return "".join(result).rstrip()
|
|
496
|
+
|
|
497
|
+
def _should_break_at_comment(self, char: str, state: dict[str, t.Any]) -> bool:
|
|
498
|
+
"""Check if we should break at a comment character."""
|
|
499
|
+
return not state["in_string"] and char == "#"
|
|
500
|
+
|
|
501
|
+
def _update_string_state(
|
|
502
|
+
self,
|
|
503
|
+
char: str,
|
|
504
|
+
index: int,
|
|
505
|
+
line: str,
|
|
506
|
+
state: dict[str, t.Any],
|
|
507
|
+
) -> None:
|
|
508
|
+
"""Update string parsing state based on current character."""
|
|
509
|
+
if self._is_string_start(char, state):
|
|
510
|
+
state["in_string"], state["quote_char"] = True, char
|
|
511
|
+
elif self._is_string_end(char, index, line, state):
|
|
512
|
+
state["in_string"], state["quote_char"] = False, None
|
|
513
|
+
|
|
514
|
+
def _is_string_start(self, char: str, state: dict[str, t.Any]) -> bool:
|
|
515
|
+
"""Check if character starts a string."""
|
|
516
|
+
return not state["in_string"] and char in ('"', "'")
|
|
517
|
+
|
|
518
|
+
def _is_string_end(
|
|
519
|
+
self,
|
|
520
|
+
char: str,
|
|
521
|
+
index: int,
|
|
522
|
+
line: str,
|
|
523
|
+
state: dict[str, t.Any],
|
|
524
|
+
) -> bool:
|
|
525
|
+
"""Check if character ends a string."""
|
|
526
|
+
return (
|
|
527
|
+
state["in_string"]
|
|
528
|
+
and char == state["quote_char"]
|
|
529
|
+
and (index == 0 or line[index - 1] != "\\")
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
def _create_docstring_finder_class(
|
|
533
|
+
self,
|
|
534
|
+
docstring_nodes: list[ast.AST],
|
|
535
|
+
) -> type[ast.NodeVisitor]:
|
|
536
|
+
class DocstringFinder(ast.NodeVisitor):
|
|
537
|
+
def _is_docstring_node(self, node: ast.AST) -> bool:
|
|
538
|
+
body = getattr(node, "body", None)
|
|
539
|
+
return (
|
|
540
|
+
hasattr(node, "body")
|
|
541
|
+
and body is not None
|
|
542
|
+
and len(body) > 0
|
|
543
|
+
and isinstance(body[0], ast.Expr)
|
|
544
|
+
and isinstance(body[0].value, ast.Constant)
|
|
545
|
+
and isinstance(body[0].value.value, str)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def _add_if_docstring(self, node: ast.AST) -> None:
|
|
549
|
+
if self._is_docstring_node(node) and hasattr(node, "body"):
|
|
550
|
+
body: list[ast.stmt] = getattr(node, "body")
|
|
551
|
+
docstring_nodes.append(body[0])
|
|
552
|
+
self.generic_visit(node)
|
|
553
|
+
|
|
554
|
+
def visit_Module(self, node: ast.Module) -> None:
|
|
555
|
+
self._add_if_docstring(node)
|
|
556
|
+
|
|
557
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
558
|
+
self._add_if_docstring(node)
|
|
559
|
+
|
|
560
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
561
|
+
self._add_if_docstring(node)
|
|
562
|
+
|
|
563
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
564
|
+
self._add_if_docstring(node)
|
|
565
|
+
|
|
566
|
+
return DocstringFinder
|
|
567
|
+
|
|
568
|
+
def _create_whitespace_step(self) -> CleaningStepProtocol:
|
|
569
|
+
class WhitespaceStep:
|
|
570
|
+
name = "remove_extra_whitespace"
|
|
571
|
+
|
|
572
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
573
|
+
import re
|
|
574
|
+
|
|
575
|
+
lines = code.split("\n")
|
|
576
|
+
cleaned_lines: list[str] = []
|
|
577
|
+
|
|
578
|
+
empty_line_count = 0
|
|
579
|
+
|
|
580
|
+
for line in lines:
|
|
581
|
+
cleaned_line = line.rstrip()
|
|
582
|
+
|
|
583
|
+
if not cleaned_line.strip():
|
|
584
|
+
empty_line_count += 1
|
|
585
|
+
if empty_line_count <= 2:
|
|
586
|
+
cleaned_lines.append("")
|
|
587
|
+
else:
|
|
588
|
+
empty_line_count = 0
|
|
589
|
+
|
|
590
|
+
leading_whitespace = len(cleaned_line) - len(
|
|
591
|
+
cleaned_line.lstrip(),
|
|
592
|
+
)
|
|
593
|
+
content = cleaned_line.lstrip()
|
|
594
|
+
|
|
595
|
+
content = re.sub(r" {2, }", " ", content)
|
|
596
|
+
|
|
597
|
+
cleaned_line = cleaned_line[:leading_whitespace] + content
|
|
598
|
+
cleaned_lines.append(cleaned_line)
|
|
599
|
+
|
|
600
|
+
while cleaned_lines and not cleaned_lines[-1].strip():
|
|
601
|
+
cleaned_lines.pop()
|
|
602
|
+
|
|
603
|
+
result = "\n".join(cleaned_lines)
|
|
604
|
+
if result and not result.endswith("\n"):
|
|
605
|
+
result += "\n"
|
|
606
|
+
|
|
607
|
+
return result
|
|
608
|
+
|
|
609
|
+
return WhitespaceStep()
|
|
610
|
+
|
|
611
|
+
def _create_formatting_step(self) -> CleaningStepProtocol:
|
|
612
|
+
class FormattingStep:
|
|
613
|
+
name = "format_code"
|
|
614
|
+
|
|
615
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
616
|
+
import re
|
|
617
|
+
|
|
618
|
+
lines = code.split("\n")
|
|
619
|
+
formatted_lines: list[str] = []
|
|
620
|
+
|
|
621
|
+
for line in lines:
|
|
622
|
+
if line.strip():
|
|
623
|
+
leading_whitespace = len(line) - len(line.lstrip())
|
|
624
|
+
content = line.lstrip()
|
|
625
|
+
|
|
626
|
+
content = re.sub(
|
|
627
|
+
r"([ =+ \ -*/%<>!&|^ ])([ ^ =+ \ -*/%<>!&|^ ])",
|
|
628
|
+
r"\1 \2",
|
|
629
|
+
content,
|
|
630
|
+
)
|
|
631
|
+
content = re.sub(
|
|
632
|
+
r"([ ^ =+ \ -*/%<>!&|^ ])([ =+ \ -*/%<>!&|^ ])",
|
|
633
|
+
r"\1 \2",
|
|
634
|
+
content,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
content = re.sub(r", ([ ^ \n])", r", \1", content)
|
|
638
|
+
|
|
639
|
+
content = re.sub(r": ([ ^ \n: ])", r": \1", content)
|
|
640
|
+
|
|
641
|
+
content = re.sub(r" {2, }", " ", content)
|
|
642
|
+
|
|
643
|
+
formatted_line = line[:leading_whitespace] + content
|
|
644
|
+
formatted_lines.append(formatted_line)
|
|
645
|
+
else:
|
|
646
|
+
formatted_lines.append(line)
|
|
647
|
+
|
|
648
|
+
return "\n".join(formatted_lines)
|
|
649
|
+
|
|
650
|
+
return FormattingStep()
|
|
651
|
+
|
|
652
|
+
def remove_line_comments(self, code: str, file_path: Path | None = None) -> str:
|
|
653
|
+
file_path = file_path or Path("temp.py")
|
|
654
|
+
step = self._create_line_comment_step()
|
|
655
|
+
return step(code, file_path)
|
|
656
|
+
|
|
657
|
+
def remove_docstrings(self, code: str, file_path: Path | None = None) -> str:
|
|
658
|
+
file_path = file_path or Path("temp.py")
|
|
659
|
+
step = self._create_docstring_step()
|
|
660
|
+
return step(code, file_path)
|
|
661
|
+
|
|
662
|
+
def remove_extra_whitespace(self, code: str, file_path: Path | None = None) -> str:
|
|
663
|
+
file_path = file_path or Path("temp.py")
|
|
664
|
+
step = self._create_whitespace_step()
|
|
665
|
+
return step(code, file_path)
|
|
666
|
+
|
|
667
|
+
def format_code(self, code: str, file_path: Path | None = None) -> str:
|
|
668
|
+
file_path = file_path or Path("temp.py")
|
|
669
|
+
step = self._create_formatting_step()
|
|
670
|
+
return step(code, file_path)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .hooks import (
|
|
2
|
+
COMPREHENSIVE_STRATEGY,
|
|
3
|
+
FAST_STRATEGY,
|
|
4
|
+
HookConfigLoader,
|
|
5
|
+
HookDefinition,
|
|
6
|
+
HookStage,
|
|
7
|
+
HookStrategy,
|
|
8
|
+
RetryPolicy,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"COMPREHENSIVE_STRATEGY",
|
|
13
|
+
"FAST_STRATEGY",
|
|
14
|
+
"HookConfigLoader",
|
|
15
|
+
"HookDefinition",
|
|
16
|
+
"HookStage",
|
|
17
|
+
"HookStrategy",
|
|
18
|
+
"RetryPolicy",
|
|
19
|
+
]
|