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.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {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
+ ]