crackerjack 0.18.2__py3-none-any.whl → 0.45.2__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.
- crackerjack/README.md +19 -0
- crackerjack/__init__.py +96 -2
- crackerjack/__main__.py +637 -138
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +39 -0
- crackerjack/adapters/_output_paths.py +167 -0
- crackerjack/adapters/_qa_adapter_base.py +309 -0
- crackerjack/adapters/_tool_adapter_base.py +706 -0
- crackerjack/adapters/ai/README.md +65 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +853 -0
- crackerjack/adapters/complexity/README.md +53 -0
- crackerjack/adapters/complexity/__init__.py +10 -0
- crackerjack/adapters/complexity/complexipy.py +641 -0
- crackerjack/adapters/dependency/__init__.py +22 -0
- crackerjack/adapters/dependency/pip_audit.py +418 -0
- crackerjack/adapters/format/README.md +72 -0
- crackerjack/adapters/format/__init__.py +11 -0
- crackerjack/adapters/format/mdformat.py +313 -0
- crackerjack/adapters/format/ruff.py +516 -0
- crackerjack/adapters/lint/README.md +47 -0
- crackerjack/adapters/lint/__init__.py +11 -0
- crackerjack/adapters/lint/codespell.py +273 -0
- crackerjack/adapters/lsp/README.md +49 -0
- crackerjack/adapters/lsp/__init__.py +27 -0
- crackerjack/adapters/lsp/_base.py +194 -0
- crackerjack/adapters/lsp/_client.py +358 -0
- crackerjack/adapters/lsp/_manager.py +193 -0
- crackerjack/adapters/lsp/skylos.py +283 -0
- crackerjack/adapters/lsp/zuban.py +557 -0
- crackerjack/adapters/refactor/README.md +59 -0
- crackerjack/adapters/refactor/__init__.py +12 -0
- crackerjack/adapters/refactor/creosote.py +318 -0
- crackerjack/adapters/refactor/refurb.py +406 -0
- crackerjack/adapters/refactor/skylos.py +494 -0
- crackerjack/adapters/sast/README.md +132 -0
- crackerjack/adapters/sast/__init__.py +32 -0
- crackerjack/adapters/sast/_base.py +201 -0
- crackerjack/adapters/sast/bandit.py +423 -0
- crackerjack/adapters/sast/pyscn.py +405 -0
- crackerjack/adapters/sast/semgrep.py +241 -0
- crackerjack/adapters/security/README.md +111 -0
- crackerjack/adapters/security/__init__.py +17 -0
- crackerjack/adapters/security/gitleaks.py +339 -0
- crackerjack/adapters/type/README.md +52 -0
- crackerjack/adapters/type/__init__.py +12 -0
- crackerjack/adapters/type/pyrefly.py +402 -0
- crackerjack/adapters/type/ty.py +402 -0
- crackerjack/adapters/type/zuban.py +522 -0
- crackerjack/adapters/utility/README.md +51 -0
- crackerjack/adapters/utility/__init__.py +10 -0
- crackerjack/adapters/utility/checks.py +884 -0
- crackerjack/agents/README.md +264 -0
- crackerjack/agents/__init__.py +66 -0
- crackerjack/agents/architect_agent.py +238 -0
- crackerjack/agents/base.py +167 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +600 -0
- crackerjack/agents/documentation_agent.py +520 -0
- crackerjack/agents/dry_agent.py +585 -0
- crackerjack/agents/enhanced_coordinator.py +279 -0
- crackerjack/agents/enhanced_proactive_agent.py +185 -0
- crackerjack/agents/error_middleware.py +53 -0
- crackerjack/agents/formatting_agent.py +230 -0
- crackerjack/agents/helpers/__init__.py +9 -0
- crackerjack/agents/helpers/performance/__init__.py +22 -0
- crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
- crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
- crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
- crackerjack/agents/helpers/refactoring/__init__.py +22 -0
- crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
- crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
- crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
- crackerjack/agents/helpers/test_creation/__init__.py +19 -0
- crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
- crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
- crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
- crackerjack/agents/import_optimization_agent.py +1181 -0
- crackerjack/agents/performance_agent.py +325 -0
- crackerjack/agents/performance_helpers.py +205 -0
- crackerjack/agents/proactive_agent.py +55 -0
- crackerjack/agents/refactoring_agent.py +511 -0
- crackerjack/agents/refactoring_helpers.py +247 -0
- crackerjack/agents/security_agent.py +793 -0
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +570 -0
- crackerjack/agents/test_specialist_agent.py +526 -0
- crackerjack/agents/tracker.py +110 -0
- crackerjack/api.py +647 -0
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/cache_handlers.py +209 -0
- crackerjack/cli/cache_handlers_enhanced.py +680 -0
- crackerjack/cli/facade.py +162 -0
- crackerjack/cli/formatting.py +13 -0
- crackerjack/cli/handlers/__init__.py +85 -0
- crackerjack/cli/handlers/advanced.py +103 -0
- crackerjack/cli/handlers/ai_features.py +62 -0
- crackerjack/cli/handlers/analytics.py +479 -0
- crackerjack/cli/handlers/changelog.py +271 -0
- crackerjack/cli/handlers/config_handlers.py +16 -0
- crackerjack/cli/handlers/coverage.py +84 -0
- crackerjack/cli/handlers/documentation.py +280 -0
- crackerjack/cli/handlers/main_handlers.py +497 -0
- crackerjack/cli/handlers/monitoring.py +371 -0
- crackerjack/cli/handlers.py +700 -0
- crackerjack/cli/interactive.py +488 -0
- crackerjack/cli/options.py +1216 -0
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/utils.py +19 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +1307 -0
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +275 -0
- crackerjack/config/global_lock_config.py +207 -0
- crackerjack/config/hooks.py +390 -0
- crackerjack/config/loader.py +239 -0
- crackerjack/config/settings.py +141 -0
- crackerjack/config/tool_commands.py +331 -0
- crackerjack/core/README.md +393 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +738 -0
- crackerjack/core/autofix_coordinator.py +282 -0
- crackerjack/core/container.py +105 -0
- crackerjack/core/enhanced_container.py +583 -0
- crackerjack/core/file_lifecycle.py +472 -0
- crackerjack/core/performance.py +244 -0
- crackerjack/core/performance_monitor.py +357 -0
- crackerjack/core/phase_coordinator.py +1227 -0
- crackerjack/core/proactive_workflow.py +267 -0
- crackerjack/core/resource_manager.py +425 -0
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +601 -0
- crackerjack/core/session_coordinator.py +239 -0
- crackerjack/core/timeout_manager.py +563 -0
- crackerjack/core/websocket_lifecycle.py +410 -0
- crackerjack/core/workflow/__init__.py +21 -0
- crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
- crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
- crackerjack/core/workflow/workflow_issue_parser.py +714 -0
- crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
- crackerjack/core/workflow/workflow_security_gates.py +400 -0
- crackerjack/core/workflow_orchestrator.py +2243 -0
- crackerjack/data/README.md +11 -0
- crackerjack/data/__init__.py +8 -0
- crackerjack/data/models.py +79 -0
- crackerjack/data/repository.py +210 -0
- crackerjack/decorators/README.md +180 -0
- crackerjack/decorators/__init__.py +35 -0
- crackerjack/decorators/error_handling.py +649 -0
- crackerjack/decorators/error_handling_decorators.py +334 -0
- crackerjack/decorators/helpers.py +58 -0
- crackerjack/decorators/patterns.py +281 -0
- crackerjack/decorators/utils.py +58 -0
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +767 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +1065 -0
- crackerjack/dynamic_config.py +678 -0
- crackerjack/errors.py +378 -0
- crackerjack/events/README.md +11 -0
- crackerjack/events/__init__.py +16 -0
- crackerjack/events/telemetry.py +175 -0
- crackerjack/events/workflow_bus.py +346 -0
- crackerjack/exceptions/README.md +301 -0
- crackerjack/exceptions/__init__.py +5 -0
- crackerjack/exceptions/config.py +4 -0
- crackerjack/exceptions/tool_execution_error.py +245 -0
- crackerjack/executors/README.md +591 -0
- crackerjack/executors/__init__.py +13 -0
- crackerjack/executors/async_hook_executor.py +938 -0
- crackerjack/executors/cached_hook_executor.py +316 -0
- crackerjack/executors/hook_executor.py +1295 -0
- crackerjack/executors/hook_lock_manager.py +708 -0
- crackerjack/executors/individual_hook_executor.py +739 -0
- crackerjack/executors/lsp_aware_hook_executor.py +349 -0
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +433 -0
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +93 -0
- crackerjack/intelligence/README.md +557 -0
- crackerjack/intelligence/__init__.py +37 -0
- crackerjack/intelligence/adaptive_learning.py +693 -0
- crackerjack/intelligence/agent_orchestrator.py +485 -0
- crackerjack/intelligence/agent_registry.py +377 -0
- crackerjack/intelligence/agent_selector.py +439 -0
- crackerjack/intelligence/integration.py +250 -0
- crackerjack/interactive.py +719 -0
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +585 -0
- crackerjack/managers/publish_manager.py +631 -0
- crackerjack/managers/test_command_builder.py +391 -0
- crackerjack/managers/test_executor.py +474 -0
- crackerjack/managers/test_manager.py +1357 -0
- crackerjack/managers/test_progress.py +187 -0
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +352 -0
- crackerjack/mcp/client_runner.py +121 -0
- crackerjack/mcp/context.py +802 -0
- crackerjack/mcp/dashboard.py +657 -0
- crackerjack/mcp/enhanced_progress_monitor.py +493 -0
- crackerjack/mcp/file_monitor.py +394 -0
- crackerjack/mcp/progress_components.py +607 -0
- crackerjack/mcp/progress_monitor.py +1016 -0
- crackerjack/mcp/rate_limiter.py +336 -0
- crackerjack/mcp/server.py +24 -0
- crackerjack/mcp/server_core.py +526 -0
- crackerjack/mcp/service_watchdog.py +505 -0
- crackerjack/mcp/state.py +407 -0
- crackerjack/mcp/task_manager.py +259 -0
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +19 -0
- crackerjack/mcp/tools/core_tools.py +469 -0
- crackerjack/mcp/tools/error_analyzer.py +283 -0
- crackerjack/mcp/tools/execution_tools.py +384 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +46 -0
- crackerjack/mcp/tools/intelligence_tools.py +264 -0
- crackerjack/mcp/tools/monitoring_tools.py +628 -0
- crackerjack/mcp/tools/proactive_tools.py +367 -0
- crackerjack/mcp/tools/progress_tools.py +222 -0
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +358 -0
- crackerjack/mcp/tools/workflow_executor.py +699 -0
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +54 -0
- crackerjack/mcp/websocket/endpoints.py +492 -0
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +406 -0
- crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
- crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
- crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
- crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
- crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
- crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
- crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
- crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
- crackerjack/mcp/websocket/monitoring/factory.py +109 -0
- crackerjack/mcp/websocket/monitoring/filters.py +10 -0
- crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
- crackerjack/mcp/websocket/monitoring/models.py +90 -0
- crackerjack/mcp/websocket/monitoring/utils.py +171 -0
- crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
- crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
- crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
- crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
- crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
- crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
- crackerjack/mcp/websocket/monitoring_endpoints.py +21 -0
- crackerjack/mcp/websocket/server.py +174 -0
- crackerjack/mcp/websocket/websocket_handler.py +276 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +40 -0
- crackerjack/models/config.py +730 -0
- crackerjack/models/config_adapter.py +265 -0
- crackerjack/models/protocols.py +1535 -0
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/resource_protocols.py +299 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +173 -0
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +405 -0
- crackerjack/monitoring/metrics_collector.py +427 -0
- crackerjack/monitoring/regression_prevention.py +580 -0
- crackerjack/monitoring/websocket_server.py +406 -0
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +894 -0
- crackerjack/orchestration/cache/README.md +312 -0
- crackerjack/orchestration/cache/__init__.py +37 -0
- crackerjack/orchestration/cache/memory_cache.py +338 -0
- crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
- crackerjack/orchestration/config.py +297 -0
- crackerjack/orchestration/coverage_improvement.py +180 -0
- crackerjack/orchestration/execution_strategies.py +361 -0
- crackerjack/orchestration/hook_orchestrator.py +1398 -0
- crackerjack/orchestration/strategies/README.md +401 -0
- crackerjack/orchestration/strategies/__init__.py +39 -0
- crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
- crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
- crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
- crackerjack/orchestration/test_progress_streamer.py +647 -0
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +254 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +264 -0
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/security/__init__.py +0 -0
- crackerjack/security/audit.py +197 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +9 -0
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/ai/contextual_ai_assistant.py +542 -0
- crackerjack/services/ai/embeddings.py +444 -0
- crackerjack/services/ai/intelligent_commit.py +328 -0
- crackerjack/services/ai/predictive_analytics.py +510 -0
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +617 -0
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +530 -0
- crackerjack/services/cache.py +369 -0
- crackerjack/services/changelog_automation.py +399 -0
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +132 -0
- crackerjack/services/config_merge.py +546 -0
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +493 -0
- crackerjack/services/coverage_badge_service.py +173 -0
- crackerjack/services/coverage_ratchet.py +381 -0
- crackerjack/services/debug.py +733 -0
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +622 -0
- crackerjack/services/documentation_generator.py +493 -0
- crackerjack/services/documentation_service.py +704 -0
- crackerjack/services/enhanced_filesystem.py +497 -0
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +149 -0
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +381 -0
- crackerjack/services/git.py +422 -0
- crackerjack/services/health_metrics.py +615 -0
- crackerjack/services/heatmap_generator.py +744 -0
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +823 -0
- crackerjack/services/input_validator.py +668 -0
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +289 -0
- crackerjack/services/logging.py +228 -0
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +414 -0
- crackerjack/services/metrics.py +587 -0
- crackerjack/services/monitoring/README.md +30 -0
- crackerjack/services/monitoring/__init__.py +9 -0
- crackerjack/services/monitoring/dependency_monitor.py +678 -0
- crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
- crackerjack/services/monitoring/health_metrics.py +716 -0
- crackerjack/services/monitoring/metrics.py +587 -0
- crackerjack/services/monitoring/performance_benchmarks.py +410 -0
- crackerjack/services/monitoring/performance_cache.py +388 -0
- crackerjack/services/monitoring/performance_monitor.py +569 -0
- crackerjack/services/parallel_executor.py +527 -0
- crackerjack/services/pattern_cache.py +333 -0
- crackerjack/services/pattern_detector.py +478 -0
- crackerjack/services/patterns/__init__.py +142 -0
- crackerjack/services/patterns/agents.py +107 -0
- crackerjack/services/patterns/code/__init__.py +15 -0
- crackerjack/services/patterns/code/detection.py +118 -0
- crackerjack/services/patterns/code/imports.py +107 -0
- crackerjack/services/patterns/code/paths.py +159 -0
- crackerjack/services/patterns/code/performance.py +119 -0
- crackerjack/services/patterns/code/replacement.py +36 -0
- crackerjack/services/patterns/core.py +212 -0
- crackerjack/services/patterns/documentation/__init__.py +14 -0
- crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
- crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
- crackerjack/services/patterns/documentation/docstrings.py +89 -0
- crackerjack/services/patterns/formatting.py +226 -0
- crackerjack/services/patterns/operations.py +339 -0
- crackerjack/services/patterns/security/__init__.py +23 -0
- crackerjack/services/patterns/security/code_injection.py +122 -0
- crackerjack/services/patterns/security/credentials.py +190 -0
- crackerjack/services/patterns/security/path_traversal.py +221 -0
- crackerjack/services/patterns/security/unsafe_operations.py +216 -0
- crackerjack/services/patterns/templates.py +62 -0
- crackerjack/services/patterns/testing/__init__.py +18 -0
- crackerjack/services/patterns/testing/error_patterns.py +107 -0
- crackerjack/services/patterns/testing/pytest_output.py +126 -0
- crackerjack/services/patterns/tool_output/__init__.py +16 -0
- crackerjack/services/patterns/tool_output/bandit.py +72 -0
- crackerjack/services/patterns/tool_output/other.py +97 -0
- crackerjack/services/patterns/tool_output/pyright.py +67 -0
- crackerjack/services/patterns/tool_output/ruff.py +44 -0
- crackerjack/services/patterns/url_sanitization.py +114 -0
- crackerjack/services/patterns/utilities.py +42 -0
- crackerjack/services/patterns/utils.py +339 -0
- crackerjack/services/patterns/validation.py +46 -0
- crackerjack/services/patterns/versioning.py +62 -0
- crackerjack/services/predictive_analytics.py +523 -0
- crackerjack/services/profiler.py +280 -0
- crackerjack/services/quality/README.md +415 -0
- crackerjack/services/quality/__init__.py +11 -0
- crackerjack/services/quality/anomaly_detector.py +392 -0
- crackerjack/services/quality/pattern_cache.py +333 -0
- crackerjack/services/quality/pattern_detector.py +479 -0
- crackerjack/services/quality/qa_orchestrator.py +491 -0
- crackerjack/services/quality/quality_baseline.py +395 -0
- crackerjack/services/quality/quality_baseline_enhanced.py +649 -0
- crackerjack/services/quality/quality_intelligence.py +949 -0
- crackerjack/services/regex_patterns.py +58 -0
- crackerjack/services/regex_utils.py +483 -0
- crackerjack/services/secure_path_utils.py +524 -0
- crackerjack/services/secure_status_formatter.py +450 -0
- crackerjack/services/secure_subprocess.py +635 -0
- crackerjack/services/security.py +239 -0
- crackerjack/services/security_logger.py +495 -0
- crackerjack/services/server_manager.py +411 -0
- crackerjack/services/smart_scheduling.py +167 -0
- crackerjack/services/status_authentication.py +460 -0
- crackerjack/services/status_security_manager.py +315 -0
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +441 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +43 -0
- crackerjack/services/unified_config.py +115 -0
- crackerjack/services/validation_rate_limiter.py +220 -0
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +461 -0
- crackerjack/services/version_checker.py +223 -0
- crackerjack/services/websocket_resource_limiter.py +438 -0
- crackerjack/services/zuban_lsp_service.py +391 -0
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/__init__.py +59 -0
- crackerjack/slash_commands/init.md +112 -0
- crackerjack/slash_commands/run.md +197 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack/tools/README.md +11 -0
- crackerjack/tools/__init__.py +30 -0
- crackerjack/tools/_git_utils.py +105 -0
- crackerjack/tools/check_added_large_files.py +139 -0
- crackerjack/tools/check_ast.py +105 -0
- crackerjack/tools/check_json.py +103 -0
- crackerjack/tools/check_jsonschema.py +297 -0
- crackerjack/tools/check_toml.py +103 -0
- crackerjack/tools/check_yaml.py +110 -0
- crackerjack/tools/codespell_wrapper.py +72 -0
- crackerjack/tools/end_of_file_fixer.py +202 -0
- crackerjack/tools/format_json.py +128 -0
- crackerjack/tools/mdformat_wrapper.py +114 -0
- crackerjack/tools/trailing_whitespace.py +198 -0
- crackerjack/tools/validate_input_validator_patterns.py +236 -0
- crackerjack/tools/validate_regex_patterns.py +188 -0
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/__init__.py +1 -0
- crackerjack/ui/dashboard_renderer.py +28 -0
- crackerjack/ui/templates/README.md +11 -0
- crackerjack/utils/console_utils.py +13 -0
- crackerjack/utils/dependency_guard.py +230 -0
- crackerjack/utils/retry_utils.py +275 -0
- crackerjack/workflows/README.md +590 -0
- crackerjack/workflows/__init__.py +46 -0
- crackerjack/workflows/actions.py +811 -0
- crackerjack/workflows/auto_fix.py +444 -0
- crackerjack/workflows/container_builder.py +499 -0
- crackerjack/workflows/definitions.py +443 -0
- crackerjack/workflows/engine.py +177 -0
- crackerjack/workflows/event_bridge.py +242 -0
- crackerjack-0.45.2.dist-info/METADATA +1678 -0
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack-0.45.2.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -14
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config.yaml +0 -91
- crackerjack/.pytest_cache/.gitignore +0 -2
- crackerjack/.pytest_cache/CACHEDIR.TAG +0 -4
- crackerjack/.pytest_cache/README.md +0 -8
- crackerjack/.pytest_cache/v/cache/nodeids +0 -1
- crackerjack/.pytest_cache/v/cache/stepwise +0 -1
- crackerjack/.ruff_cache/.gitignore +0 -1
- crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
- crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
- crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
- crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
- crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
- crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
- crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
- crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
- crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
- crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
- crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
- crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
- crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
- crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
- crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
- crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
- crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
- crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
- crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
- crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
- crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
- crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
- crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
- crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
- crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
- crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
- crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
- crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
- crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
- crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
- crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
- crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
- crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
- crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
- crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
- crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
- crackerjack/.ruff_cache/CACHEDIR.TAG +0 -1
- crackerjack/crackerjack.py +0 -855
- crackerjack/pyproject.toml +0 -214
- crackerjack-0.18.2.dist-info/METADATA +0 -420
- crackerjack-0.18.2.dist-info/RECORD +0 -59
- crackerjack-0.18.2.dist-info/entry_points.txt +0 -4
- {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import typing as t
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
from .errors import ErrorCode, ExecutionError
|
|
10
|
+
from .services.backup_service import BackupMetadata, PackageBackupService
|
|
11
|
+
from .services.regex_patterns import SAFE_PATTERNS
|
|
12
|
+
from .services.secure_path_utils import (
|
|
13
|
+
AtomicFileOperations,
|
|
14
|
+
SecurePathValidator,
|
|
15
|
+
)
|
|
16
|
+
from .services.security_logger import (
|
|
17
|
+
SecurityEventLevel,
|
|
18
|
+
SecurityEventType,
|
|
19
|
+
get_security_logger,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SafePatternApplicator:
|
|
24
|
+
def apply_docstring_patterns(self, code: str) -> str:
|
|
25
|
+
# Intentionally a no-op for docstrings here. Actual docstring removal is
|
|
26
|
+
# handled by the structured AST cleaning step (_create_docstring_step).
|
|
27
|
+
# This keeps SafePatternApplicator focused on formatting-only changes.
|
|
28
|
+
return code
|
|
29
|
+
|
|
30
|
+
def apply_formatting_patterns(self, content: str) -> str:
|
|
31
|
+
content = SAFE_PATTERNS["spacing_after_comma"].apply(content)
|
|
32
|
+
content = SAFE_PATTERNS["spacing_after_colon"].apply(content)
|
|
33
|
+
content = SAFE_PATTERNS["multiple_spaces"].apply(content)
|
|
34
|
+
return content
|
|
35
|
+
|
|
36
|
+
def has_preserved_comment(self, line: str) -> bool:
|
|
37
|
+
# Preserve shebangs (still useful for executable scripts)
|
|
38
|
+
if line.strip().startswith(("#! /", "#!/")):
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
line_lower = line.lower()
|
|
42
|
+
preserved_keywords = [
|
|
43
|
+
# Security & linting directives (critical)
|
|
44
|
+
"nosec",
|
|
45
|
+
"noqa",
|
|
46
|
+
"pragma",
|
|
47
|
+
# Type checking directives (critical)
|
|
48
|
+
"type: ",
|
|
49
|
+
# Security markers (custom)
|
|
50
|
+
"regex ok",
|
|
51
|
+
# Task tracking (useful)
|
|
52
|
+
"todo",
|
|
53
|
+
# Note: "coding:" and "encoding:" removed - obsolete since Python 3.0
|
|
54
|
+
]
|
|
55
|
+
return any(keyword in line_lower for keyword in preserved_keywords)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_safe_applicator = SafePatternApplicator()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CleaningResult:
|
|
63
|
+
file_path: Path
|
|
64
|
+
success: bool
|
|
65
|
+
steps_completed: list[str]
|
|
66
|
+
steps_failed: list[str]
|
|
67
|
+
warnings: list[str]
|
|
68
|
+
original_size: int
|
|
69
|
+
cleaned_size: int
|
|
70
|
+
backup_metadata: BackupMetadata | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class PackageCleaningResult:
|
|
75
|
+
total_files: int
|
|
76
|
+
successful_files: int
|
|
77
|
+
failed_files: int
|
|
78
|
+
file_results: list[CleaningResult]
|
|
79
|
+
backup_metadata: BackupMetadata | None
|
|
80
|
+
backup_restored: bool = False
|
|
81
|
+
overall_success: bool = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CleaningStepProtocol(Protocol):
|
|
85
|
+
def __call__(self, code: str, file_path: Path) -> str: ...
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def name(self) -> str: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FileProcessor(BaseModel):
|
|
92
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
|
93
|
+
|
|
94
|
+
console: t.Any
|
|
95
|
+
logger: t.Any = None
|
|
96
|
+
base_directory: Path | None = None
|
|
97
|
+
security_logger: t.Any = None
|
|
98
|
+
|
|
99
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
100
|
+
if self.logger is None:
|
|
101
|
+
import logging
|
|
102
|
+
|
|
103
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner.file_processor")
|
|
104
|
+
|
|
105
|
+
if self.security_logger is None:
|
|
106
|
+
self.security_logger = get_security_logger()
|
|
107
|
+
|
|
108
|
+
def read_file_safely(self, file_path: Path) -> str:
|
|
109
|
+
validated_path = SecurePathValidator.validate_file_path(
|
|
110
|
+
file_path, self.base_directory
|
|
111
|
+
)
|
|
112
|
+
SecurePathValidator.validate_file_size(validated_path)
|
|
113
|
+
|
|
114
|
+
self.security_logger.log_security_event(
|
|
115
|
+
SecurityEventType.FILE_CLEANED,
|
|
116
|
+
SecurityEventLevel.LOW,
|
|
117
|
+
f"Reading file for cleaning: {validated_path}",
|
|
118
|
+
file_path=validated_path,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
return validated_path.read_text(encoding="utf-8")
|
|
123
|
+
|
|
124
|
+
except UnicodeDecodeError:
|
|
125
|
+
for encoding in ("latin1", "cp1252"):
|
|
126
|
+
try:
|
|
127
|
+
content = validated_path.read_text(encoding=encoding)
|
|
128
|
+
self.logger.warning(
|
|
129
|
+
f"File {validated_path} read with {encoding} encoding",
|
|
130
|
+
)
|
|
131
|
+
return content
|
|
132
|
+
except UnicodeDecodeError:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
self.security_logger.log_validation_failed(
|
|
136
|
+
"encoding",
|
|
137
|
+
file_path,
|
|
138
|
+
"Could not decode file with any supported encoding",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
raise ExecutionError(
|
|
142
|
+
message=f"Could not decode file {file_path}",
|
|
143
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
except ExecutionError:
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
self.security_logger.log_validation_failed(
|
|
151
|
+
"file_read", file_path, f"Unexpected error during file read: {e}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
raise ExecutionError(
|
|
155
|
+
message=f"Failed to read file {file_path}: {e}",
|
|
156
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
157
|
+
) from e
|
|
158
|
+
|
|
159
|
+
def write_file_safely(self, file_path: Path, content: str) -> None:
|
|
160
|
+
try:
|
|
161
|
+
AtomicFileOperations.atomic_write(file_path, content, self.base_directory)
|
|
162
|
+
|
|
163
|
+
self.security_logger.log_atomic_operation("write", file_path, True)
|
|
164
|
+
|
|
165
|
+
except ExecutionError:
|
|
166
|
+
self.security_logger.log_atomic_operation("write", file_path, False)
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.security_logger.log_atomic_operation(
|
|
171
|
+
"write", file_path, False, error=str(e)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
raise ExecutionError(
|
|
175
|
+
message=f"Failed to write file {file_path}: {e}",
|
|
176
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
177
|
+
) from e
|
|
178
|
+
|
|
179
|
+
def backup_file(self, file_path: Path) -> Path:
|
|
180
|
+
try:
|
|
181
|
+
backup_path = AtomicFileOperations.atomic_backup_and_write(
|
|
182
|
+
file_path, file_path.read_bytes(), self.base_directory
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self.security_logger.log_backup_created(file_path, backup_path)
|
|
186
|
+
|
|
187
|
+
return backup_path
|
|
188
|
+
|
|
189
|
+
except ExecutionError:
|
|
190
|
+
raise
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self.security_logger.log_validation_failed(
|
|
194
|
+
"backup_creation", file_path, f"Backup creation failed: {e}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
raise ExecutionError(
|
|
198
|
+
message=f"Failed to create backup for {file_path}: {e}",
|
|
199
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
200
|
+
) from e
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class CleaningErrorHandler(BaseModel):
|
|
204
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
205
|
+
|
|
206
|
+
console: t.Any
|
|
207
|
+
logger: t.Any = None
|
|
208
|
+
|
|
209
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
210
|
+
if self.logger is None:
|
|
211
|
+
import logging
|
|
212
|
+
|
|
213
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner.error_handler")
|
|
214
|
+
|
|
215
|
+
def handle_file_error(self, file_path: Path, error: Exception, step: str) -> None:
|
|
216
|
+
self.console.print(
|
|
217
|
+
f"[bold bright_yellow]⚠️ Warning: {step} failed for {file_path}: {error}[/ bold bright_yellow]",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self.logger.warning(
|
|
221
|
+
"Cleaning step failed",
|
|
222
|
+
extra={
|
|
223
|
+
"file_path": str(file_path),
|
|
224
|
+
"step": step,
|
|
225
|
+
"error": str(error),
|
|
226
|
+
"error_type": type(error).__name__,
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def log_cleaning_result(self, result: CleaningResult) -> None:
|
|
231
|
+
if result.success:
|
|
232
|
+
self.console.print(
|
|
233
|
+
f"[green]✅ Cleaned {result.file_path}[/ green] "
|
|
234
|
+
f"({result.original_size} → {result.cleaned_size} bytes)",
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
self.console.print(
|
|
238
|
+
f"[red]❌ Failed to clean {result.file_path}[/ red] "
|
|
239
|
+
f"({len(result.steps_failed)} steps failed)",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if result.warnings:
|
|
243
|
+
for warning in result.warnings:
|
|
244
|
+
self.console.print(f"[yellow]⚠️ {warning}[/ yellow]")
|
|
245
|
+
|
|
246
|
+
self.logger.info(
|
|
247
|
+
"File cleaning completed",
|
|
248
|
+
extra={
|
|
249
|
+
"file_path": str(result.file_path),
|
|
250
|
+
"success": result.success,
|
|
251
|
+
"steps_completed": result.steps_completed,
|
|
252
|
+
"steps_failed": result.steps_failed,
|
|
253
|
+
"original_size": result.original_size,
|
|
254
|
+
"cleaned_size": result.cleaned_size,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class CleaningPipeline(BaseModel):
|
|
260
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
261
|
+
|
|
262
|
+
file_processor: t.Any
|
|
263
|
+
error_handler: t.Any
|
|
264
|
+
console: t.Any
|
|
265
|
+
logger: t.Any = None
|
|
266
|
+
|
|
267
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
268
|
+
if self.logger is None:
|
|
269
|
+
import logging
|
|
270
|
+
|
|
271
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner.pipeline")
|
|
272
|
+
|
|
273
|
+
def clean_file(
|
|
274
|
+
self,
|
|
275
|
+
file_path: Path,
|
|
276
|
+
cleaning_steps: list[CleaningStepProtocol],
|
|
277
|
+
) -> CleaningResult:
|
|
278
|
+
self.logger.info(f"Starting clean_file for {file_path}")
|
|
279
|
+
try:
|
|
280
|
+
original_code = self.file_processor.read_file_safely(file_path)
|
|
281
|
+
original_size = len(original_code.encode("utf-8"))
|
|
282
|
+
|
|
283
|
+
result = self._apply_cleaning_pipeline(
|
|
284
|
+
original_code,
|
|
285
|
+
file_path,
|
|
286
|
+
cleaning_steps,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
cleaned_size = original_size
|
|
290
|
+
if result.success and result.cleaned_code != original_code:
|
|
291
|
+
self.file_processor.write_file_safely(file_path, result.cleaned_code)
|
|
292
|
+
cleaned_size = len(result.cleaned_code.encode("utf-8"))
|
|
293
|
+
|
|
294
|
+
cleaning_result = CleaningResult(
|
|
295
|
+
file_path=file_path,
|
|
296
|
+
success=result.success,
|
|
297
|
+
steps_completed=result.steps_completed,
|
|
298
|
+
steps_failed=result.steps_failed,
|
|
299
|
+
warnings=result.warnings,
|
|
300
|
+
original_size=original_size,
|
|
301
|
+
cleaned_size=cleaned_size,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
self.error_handler.log_cleaning_result(cleaning_result)
|
|
305
|
+
return cleaning_result
|
|
306
|
+
|
|
307
|
+
except Exception as e:
|
|
308
|
+
self.error_handler.handle_file_error(file_path, e, "file_processing")
|
|
309
|
+
return CleaningResult(
|
|
310
|
+
file_path=file_path,
|
|
311
|
+
success=False,
|
|
312
|
+
steps_completed=[],
|
|
313
|
+
steps_failed=["file_processing"],
|
|
314
|
+
warnings=[],
|
|
315
|
+
original_size=0,
|
|
316
|
+
cleaned_size=0,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class PipelineResult:
|
|
321
|
+
cleaned_code: str
|
|
322
|
+
success: bool
|
|
323
|
+
steps_completed: list[str]
|
|
324
|
+
steps_failed: list[str]
|
|
325
|
+
warnings: list[str]
|
|
326
|
+
|
|
327
|
+
def _apply_cleaning_pipeline(
|
|
328
|
+
self,
|
|
329
|
+
code: str,
|
|
330
|
+
file_path: Path,
|
|
331
|
+
cleaning_steps: list[CleaningStepProtocol],
|
|
332
|
+
) -> PipelineResult:
|
|
333
|
+
current_code = code
|
|
334
|
+
steps_completed: list[str] = []
|
|
335
|
+
steps_failed: list[str] = []
|
|
336
|
+
warnings: list[str] = []
|
|
337
|
+
overall_success = True
|
|
338
|
+
|
|
339
|
+
for step in cleaning_steps:
|
|
340
|
+
try:
|
|
341
|
+
step_result = step(current_code, file_path)
|
|
342
|
+
current_code = step_result
|
|
343
|
+
steps_completed.append(step.name)
|
|
344
|
+
|
|
345
|
+
self.logger.debug(
|
|
346
|
+
"Cleaning step completed",
|
|
347
|
+
extra={"step": step.name, "file_path": str(file_path)},
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
self.error_handler.handle_file_error(file_path, e, step.name)
|
|
352
|
+
steps_failed.append(step.name)
|
|
353
|
+
warnings.append(f"{step.name} failed: {e}")
|
|
354
|
+
|
|
355
|
+
self.logger.warning(
|
|
356
|
+
"Cleaning step failed, continuing with original code",
|
|
357
|
+
extra={
|
|
358
|
+
"step": step.name,
|
|
359
|
+
"file_path": str(file_path),
|
|
360
|
+
"error": str(e),
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if steps_failed:
|
|
365
|
+
success_ratio = len(steps_completed) / (
|
|
366
|
+
len(steps_completed) + len(steps_failed)
|
|
367
|
+
)
|
|
368
|
+
overall_success = success_ratio >= 0.7
|
|
369
|
+
|
|
370
|
+
return self.PipelineResult(
|
|
371
|
+
cleaned_code=current_code,
|
|
372
|
+
success=overall_success,
|
|
373
|
+
steps_completed=steps_completed,
|
|
374
|
+
steps_failed=steps_failed,
|
|
375
|
+
warnings=warnings,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class CodeCleaner(BaseModel):
|
|
380
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
|
381
|
+
|
|
382
|
+
console: t.Any
|
|
383
|
+
file_processor: t.Any = None
|
|
384
|
+
error_handler: t.Any = None
|
|
385
|
+
pipeline: t.Any = None
|
|
386
|
+
logger: t.Any = None
|
|
387
|
+
base_directory: Path | None = None
|
|
388
|
+
security_logger: t.Any = None
|
|
389
|
+
backup_service: t.Any = None
|
|
390
|
+
|
|
391
|
+
def model_post_init(self, _: t.Any) -> None:
|
|
392
|
+
if self.logger is None:
|
|
393
|
+
import logging
|
|
394
|
+
|
|
395
|
+
self.logger = logging.getLogger("crackerjack.code_cleaner")
|
|
396
|
+
|
|
397
|
+
if self.base_directory is None:
|
|
398
|
+
self.base_directory = Path.cwd()
|
|
399
|
+
|
|
400
|
+
if self.file_processor is None:
|
|
401
|
+
self.file_processor = FileProcessor(
|
|
402
|
+
console=self.console, base_directory=self.base_directory
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if self.error_handler is None:
|
|
406
|
+
self.error_handler = CleaningErrorHandler(console=self.console)
|
|
407
|
+
|
|
408
|
+
if self.pipeline is None:
|
|
409
|
+
self.pipeline = CleaningPipeline(
|
|
410
|
+
file_processor=self.file_processor,
|
|
411
|
+
error_handler=self.error_handler,
|
|
412
|
+
console=self.console,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if self.security_logger is None:
|
|
416
|
+
self.security_logger = get_security_logger()
|
|
417
|
+
|
|
418
|
+
if self.backup_service is None:
|
|
419
|
+
self.backup_service = PackageBackupService()
|
|
420
|
+
|
|
421
|
+
def clean_file(self, file_path: Path) -> CleaningResult:
|
|
422
|
+
cleaning_steps = [
|
|
423
|
+
self._create_line_comment_step(),
|
|
424
|
+
self._create_docstring_step(),
|
|
425
|
+
self._create_whitespace_step(),
|
|
426
|
+
self._create_formatting_step(),
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
result = self.pipeline.clean_file(file_path, cleaning_steps)
|
|
430
|
+
return t.cast(CleaningResult, result)
|
|
431
|
+
|
|
432
|
+
def clean_files(
|
|
433
|
+
self, pkg_dir: Path | None = None, use_backup: bool = True
|
|
434
|
+
) -> list[CleaningResult] | PackageCleaningResult:
|
|
435
|
+
if use_backup:
|
|
436
|
+
package_result = self.clean_files_with_backup(pkg_dir)
|
|
437
|
+
self.logger.info(
|
|
438
|
+
f"Package cleaning with backup completed: "
|
|
439
|
+
f"success={package_result.overall_success}, "
|
|
440
|
+
f"restored={package_result.backup_restored}"
|
|
441
|
+
)
|
|
442
|
+
return package_result
|
|
443
|
+
|
|
444
|
+
self.console.print(
|
|
445
|
+
"[yellow]⚠️ WARNING: Running without backup protection. "
|
|
446
|
+
"Consider using use_backup=True for safety.[/yellow]"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if pkg_dir is None:
|
|
450
|
+
# Use configured base directory when no explicit path is provided
|
|
451
|
+
pkg_dir = self.base_directory or Path.cwd()
|
|
452
|
+
|
|
453
|
+
python_files = self._discover_package_files(pkg_dir)
|
|
454
|
+
|
|
455
|
+
files_to_process = [
|
|
456
|
+
file_path
|
|
457
|
+
for file_path in python_files
|
|
458
|
+
if self.should_process_file(file_path)
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
results: list[CleaningResult] = []
|
|
462
|
+
self.logger.info(f"Starting clean_files for {len(files_to_process)} files")
|
|
463
|
+
|
|
464
|
+
cleaning_steps = [
|
|
465
|
+
self._create_line_comment_step(),
|
|
466
|
+
self._create_docstring_step(),
|
|
467
|
+
self._create_whitespace_step(),
|
|
468
|
+
self._create_formatting_step(),
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
for file_path in files_to_process:
|
|
472
|
+
result = self.pipeline.clean_file(file_path, cleaning_steps)
|
|
473
|
+
results.append(result)
|
|
474
|
+
|
|
475
|
+
return results
|
|
476
|
+
|
|
477
|
+
def clean_files_with_backup(
|
|
478
|
+
self, pkg_dir: Path | None = None
|
|
479
|
+
) -> PackageCleaningResult:
|
|
480
|
+
validated_pkg_dir = self._prepare_package_directory(pkg_dir)
|
|
481
|
+
|
|
482
|
+
self.logger.info(
|
|
483
|
+
f"Starting safe package cleaning with backup: {validated_pkg_dir}"
|
|
484
|
+
)
|
|
485
|
+
self.console.print(
|
|
486
|
+
"[cyan]🛡️ Starting package cleaning with backup protection...[/cyan]"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
backup_metadata: BackupMetadata | None = None
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
backup_metadata = self._create_backup(validated_pkg_dir)
|
|
493
|
+
files_to_process = self._find_files_to_process(validated_pkg_dir)
|
|
494
|
+
|
|
495
|
+
if not files_to_process:
|
|
496
|
+
return self._handle_no_files_to_process(backup_metadata)
|
|
497
|
+
|
|
498
|
+
cleaning_result = self._execute_cleaning_with_backup(
|
|
499
|
+
files_to_process, backup_metadata
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return self._finalize_cleaning_result(cleaning_result, backup_metadata)
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
return self._handle_critical_error(e, backup_metadata)
|
|
506
|
+
|
|
507
|
+
def _prepare_package_directory(self, pkg_dir: Path | None) -> Path:
|
|
508
|
+
if pkg_dir is None:
|
|
509
|
+
pkg_dir = self.base_directory or Path.cwd()
|
|
510
|
+
# Avoid normalizing symlinks to preserve exact input path semantics
|
|
511
|
+
# while still enforcing base-directory containment.
|
|
512
|
+
if self.base_directory and not SecurePathValidator.is_within_directory(
|
|
513
|
+
pkg_dir, self.base_directory
|
|
514
|
+
):
|
|
515
|
+
raise ExecutionError(
|
|
516
|
+
message=(
|
|
517
|
+
f"Path outside allowed directory: {pkg_dir} not within "
|
|
518
|
+
f"{self.base_directory}"
|
|
519
|
+
),
|
|
520
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
521
|
+
)
|
|
522
|
+
return pkg_dir
|
|
523
|
+
|
|
524
|
+
def _create_backup(self, validated_pkg_dir: Path) -> BackupMetadata:
|
|
525
|
+
self.console.print(
|
|
526
|
+
"[yellow]📦 Creating backup of all package files...[/yellow]"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
backup_result = self.backup_service.create_package_backup(
|
|
530
|
+
validated_pkg_dir, self.base_directory
|
|
531
|
+
)
|
|
532
|
+
backup_metadata: BackupMetadata = t.cast(BackupMetadata, backup_result)
|
|
533
|
+
|
|
534
|
+
self.console.print(
|
|
535
|
+
f"[green]✅ Backup created: {backup_metadata.backup_id}[/green] "
|
|
536
|
+
f"({backup_metadata.total_files} files, {backup_metadata.total_size} bytes)"
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return backup_metadata
|
|
540
|
+
|
|
541
|
+
def _find_files_to_process(self, validated_pkg_dir: Path) -> list[Path]:
|
|
542
|
+
python_files = self._discover_package_files(validated_pkg_dir)
|
|
543
|
+
return [
|
|
544
|
+
file_path
|
|
545
|
+
for file_path in python_files
|
|
546
|
+
if self.should_process_file(file_path)
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
def _discover_package_files(self, root_dir: Path) -> list[Path]:
|
|
550
|
+
package_dir = self._find_package_directory(root_dir)
|
|
551
|
+
|
|
552
|
+
if not package_dir or not package_dir.exists():
|
|
553
|
+
self.console.print(
|
|
554
|
+
"[yellow]⚠️ Could not determine package directory, searching for Python packages...[/yellow]"
|
|
555
|
+
)
|
|
556
|
+
return self._fallback_discover_packages(root_dir)
|
|
557
|
+
|
|
558
|
+
self.logger.debug(f"Using package directory: {package_dir}")
|
|
559
|
+
|
|
560
|
+
package_files = list[t.Any](package_dir.rglob("*.py"))
|
|
561
|
+
|
|
562
|
+
exclude_dirs = {
|
|
563
|
+
"__pycache__",
|
|
564
|
+
".pytest_cache",
|
|
565
|
+
".mypy_cache",
|
|
566
|
+
".ruff_cache",
|
|
567
|
+
".venv",
|
|
568
|
+
"venv",
|
|
569
|
+
}
|
|
570
|
+
filtered_files = [
|
|
571
|
+
f
|
|
572
|
+
for f in package_files
|
|
573
|
+
if not any(excl in f.parts for excl in exclude_dirs)
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
return filtered_files
|
|
577
|
+
|
|
578
|
+
def _find_package_directory(self, root_dir: Path) -> Path | None:
|
|
579
|
+
pyproject_path = root_dir / "pyproject.toml"
|
|
580
|
+
if pyproject_path.exists():
|
|
581
|
+
try:
|
|
582
|
+
import tomllib
|
|
583
|
+
|
|
584
|
+
with pyproject_path.open("rb") as f:
|
|
585
|
+
config = tomllib.load(f)
|
|
586
|
+
|
|
587
|
+
project_name_raw = config.get("project", {}).get("name")
|
|
588
|
+
project_name: str | None = t.cast(str | None, project_name_raw)
|
|
589
|
+
if project_name:
|
|
590
|
+
package_name = project_name.replace("-", "_").lower()
|
|
591
|
+
package_dir = root_dir / package_name
|
|
592
|
+
|
|
593
|
+
if package_dir.exists() and (package_dir / "__init__.py").exists():
|
|
594
|
+
return package_dir
|
|
595
|
+
|
|
596
|
+
except Exception as e:
|
|
597
|
+
self.logger.debug(f"Could not parse pyproject.toml: {e}")
|
|
598
|
+
|
|
599
|
+
package_name = root_dir.name.replace("-", "_").lower()
|
|
600
|
+
package_dir = root_dir / package_name
|
|
601
|
+
|
|
602
|
+
if package_dir.exists() and (package_dir / "__init__.py").exists():
|
|
603
|
+
return package_dir
|
|
604
|
+
|
|
605
|
+
return None
|
|
606
|
+
|
|
607
|
+
def _fallback_discover_packages(self, root_dir: Path) -> list[Path]:
|
|
608
|
+
python_files = []
|
|
609
|
+
exclude_dirs = {
|
|
610
|
+
"__pycache__",
|
|
611
|
+
".git",
|
|
612
|
+
".venv",
|
|
613
|
+
"venv",
|
|
614
|
+
"site-packages",
|
|
615
|
+
".pytest_cache",
|
|
616
|
+
"build",
|
|
617
|
+
"dist",
|
|
618
|
+
".tox",
|
|
619
|
+
"node_modules",
|
|
620
|
+
"tests",
|
|
621
|
+
"test",
|
|
622
|
+
"examples",
|
|
623
|
+
"example",
|
|
624
|
+
"docs",
|
|
625
|
+
"doc",
|
|
626
|
+
".mypy_cache",
|
|
627
|
+
".ruff_cache",
|
|
628
|
+
"htmlcov",
|
|
629
|
+
".coverage",
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
for item in root_dir.iterdir():
|
|
633
|
+
if (
|
|
634
|
+
not item.is_dir()
|
|
635
|
+
or item.name.startswith(".")
|
|
636
|
+
or item.name in exclude_dirs
|
|
637
|
+
):
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
if (item / "__init__.py").exists():
|
|
641
|
+
package_files = [
|
|
642
|
+
f
|
|
643
|
+
for f in item.rglob("*.py")
|
|
644
|
+
if self._should_include_file_path(f, exclude_dirs)
|
|
645
|
+
]
|
|
646
|
+
python_files.extend(package_files)
|
|
647
|
+
|
|
648
|
+
return python_files
|
|
649
|
+
|
|
650
|
+
def _should_include_file_path(
|
|
651
|
+
self, file_path: Path, exclude_dirs: set[str]
|
|
652
|
+
) -> bool:
|
|
653
|
+
path_parts = set[t.Any](file_path.parts)
|
|
654
|
+
|
|
655
|
+
return not bool(path_parts.intersection(exclude_dirs))
|
|
656
|
+
|
|
657
|
+
def _handle_no_files_to_process(
|
|
658
|
+
self, backup_metadata: BackupMetadata
|
|
659
|
+
) -> PackageCleaningResult:
|
|
660
|
+
self.console.print("[yellow]⚠️ No files found to process[/yellow]")
|
|
661
|
+
self.backup_service.cleanup_backup(backup_metadata)
|
|
662
|
+
|
|
663
|
+
return PackageCleaningResult(
|
|
664
|
+
total_files=0,
|
|
665
|
+
successful_files=0,
|
|
666
|
+
failed_files=0,
|
|
667
|
+
file_results=[],
|
|
668
|
+
backup_metadata=None,
|
|
669
|
+
backup_restored=False,
|
|
670
|
+
overall_success=True,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def _execute_cleaning_with_backup(
|
|
674
|
+
self, files_to_process: list[Path], backup_metadata: BackupMetadata
|
|
675
|
+
) -> dict[str, t.Any]:
|
|
676
|
+
self.console.print(f"[cyan]🧹 Cleaning {len(files_to_process)} files...[/cyan]")
|
|
677
|
+
|
|
678
|
+
cleaning_steps = [
|
|
679
|
+
self._create_line_comment_step(),
|
|
680
|
+
self._create_docstring_step(),
|
|
681
|
+
self._create_whitespace_step(),
|
|
682
|
+
self._create_formatting_step(),
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
file_results: list[CleaningResult] = []
|
|
686
|
+
cleaning_errors: list[Exception] = []
|
|
687
|
+
|
|
688
|
+
for file_path in files_to_process:
|
|
689
|
+
try:
|
|
690
|
+
result = self.pipeline.clean_file(file_path, cleaning_steps)
|
|
691
|
+
result.backup_metadata = backup_metadata
|
|
692
|
+
file_results.append(result)
|
|
693
|
+
|
|
694
|
+
if not result.success:
|
|
695
|
+
cleaning_errors.append(
|
|
696
|
+
ExecutionError(
|
|
697
|
+
message=f"Cleaning failed for {file_path}: {result.steps_failed}",
|
|
698
|
+
error_code=ErrorCode.CODE_CLEANING_ERROR,
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
cleaning_errors.append(e)
|
|
703
|
+
file_results.append(
|
|
704
|
+
CleaningResult(
|
|
705
|
+
file_path=file_path,
|
|
706
|
+
success=False,
|
|
707
|
+
steps_completed=[],
|
|
708
|
+
steps_failed=["file_processing"],
|
|
709
|
+
warnings=[f"Exception during cleaning: {e}"],
|
|
710
|
+
original_size=0,
|
|
711
|
+
cleaned_size=0,
|
|
712
|
+
backup_metadata=backup_metadata,
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
"file_results": file_results,
|
|
718
|
+
"cleaning_errors": cleaning_errors,
|
|
719
|
+
"files_to_process": files_to_process,
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
def _finalize_cleaning_result(
|
|
723
|
+
self, cleaning_result: dict[str, t.Any], backup_metadata: BackupMetadata
|
|
724
|
+
) -> PackageCleaningResult:
|
|
725
|
+
file_results = cleaning_result["file_results"]
|
|
726
|
+
cleaning_errors = cleaning_result["cleaning_errors"]
|
|
727
|
+
files_to_process = cleaning_result["files_to_process"]
|
|
728
|
+
|
|
729
|
+
successful_files = sum(1 for result in file_results if result.success)
|
|
730
|
+
failed_files = len(file_results) - successful_files
|
|
731
|
+
|
|
732
|
+
if cleaning_errors or failed_files > 0:
|
|
733
|
+
return self._handle_cleaning_failure(
|
|
734
|
+
backup_metadata,
|
|
735
|
+
file_results,
|
|
736
|
+
files_to_process,
|
|
737
|
+
successful_files,
|
|
738
|
+
failed_files,
|
|
739
|
+
cleaning_errors,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
return self._handle_cleaning_success(
|
|
743
|
+
backup_metadata, file_results, files_to_process, successful_files
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
def _handle_cleaning_failure(
|
|
747
|
+
self,
|
|
748
|
+
backup_metadata: BackupMetadata,
|
|
749
|
+
file_results: list[CleaningResult],
|
|
750
|
+
files_to_process: list[Path],
|
|
751
|
+
successful_files: int,
|
|
752
|
+
failed_files: int,
|
|
753
|
+
cleaning_errors: list[Exception],
|
|
754
|
+
) -> PackageCleaningResult:
|
|
755
|
+
self.console.print(
|
|
756
|
+
f"[red]❌ Cleaning failed ({failed_files} files failed). "
|
|
757
|
+
f"Restoring from backup...[/red]"
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
self.logger.error(
|
|
761
|
+
f"Package cleaning failed with {len(cleaning_errors)} errors, "
|
|
762
|
+
f"restoring from backup {backup_metadata.backup_id}"
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
self.backup_service.restore_from_backup(backup_metadata, self.base_directory)
|
|
766
|
+
|
|
767
|
+
self.console.print("[green]✅ Files restored from backup successfully[/green]")
|
|
768
|
+
|
|
769
|
+
return PackageCleaningResult(
|
|
770
|
+
total_files=len(files_to_process),
|
|
771
|
+
successful_files=successful_files,
|
|
772
|
+
failed_files=failed_files,
|
|
773
|
+
file_results=file_results,
|
|
774
|
+
backup_metadata=backup_metadata,
|
|
775
|
+
backup_restored=True,
|
|
776
|
+
overall_success=False,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
def _handle_cleaning_success(
|
|
780
|
+
self,
|
|
781
|
+
backup_metadata: BackupMetadata,
|
|
782
|
+
file_results: list[CleaningResult],
|
|
783
|
+
files_to_process: list[Path],
|
|
784
|
+
successful_files: int,
|
|
785
|
+
) -> PackageCleaningResult:
|
|
786
|
+
self.console.print(
|
|
787
|
+
f"[green]✅ Package cleaning completed successfully![/green] "
|
|
788
|
+
f"({successful_files} files cleaned)"
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
self.backup_service.cleanup_backup(backup_metadata)
|
|
792
|
+
|
|
793
|
+
return PackageCleaningResult(
|
|
794
|
+
total_files=len(files_to_process),
|
|
795
|
+
successful_files=successful_files,
|
|
796
|
+
failed_files=0,
|
|
797
|
+
file_results=file_results,
|
|
798
|
+
backup_metadata=None,
|
|
799
|
+
backup_restored=False,
|
|
800
|
+
overall_success=True,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
def _handle_critical_error(
|
|
804
|
+
self, error: Exception, backup_metadata: BackupMetadata | None
|
|
805
|
+
) -> PackageCleaningResult:
|
|
806
|
+
self.logger.error(f"Critical error during package cleaning: {error}")
|
|
807
|
+
self.console.print(f"[red]💥 Critical error: {error}[/red]")
|
|
808
|
+
|
|
809
|
+
backup_restored = False
|
|
810
|
+
|
|
811
|
+
if backup_metadata:
|
|
812
|
+
backup_restored = self._attempt_emergency_restoration(backup_metadata)
|
|
813
|
+
|
|
814
|
+
return PackageCleaningResult(
|
|
815
|
+
total_files=0,
|
|
816
|
+
successful_files=0,
|
|
817
|
+
failed_files=0,
|
|
818
|
+
file_results=[],
|
|
819
|
+
backup_metadata=backup_metadata,
|
|
820
|
+
backup_restored=backup_restored,
|
|
821
|
+
overall_success=False,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
def _attempt_emergency_restoration(self, backup_metadata: BackupMetadata) -> bool:
|
|
825
|
+
try:
|
|
826
|
+
self.console.print(
|
|
827
|
+
"[yellow]🔄 Attempting emergency restoration...[/yellow]"
|
|
828
|
+
)
|
|
829
|
+
self.backup_service.restore_from_backup(
|
|
830
|
+
backup_metadata, self.base_directory
|
|
831
|
+
)
|
|
832
|
+
self.console.print("[green]✅ Emergency restoration completed[/green]")
|
|
833
|
+
return True
|
|
834
|
+
|
|
835
|
+
except Exception as restore_error:
|
|
836
|
+
self.logger.error(f"Emergency restoration failed: {restore_error}")
|
|
837
|
+
self.console.print(
|
|
838
|
+
f"[red]💥 Emergency restoration failed: {restore_error}[/red]\n"
|
|
839
|
+
f"[yellow]⚠️ Manual restoration may be needed from: "
|
|
840
|
+
f"{backup_metadata.backup_directory}[/yellow]"
|
|
841
|
+
)
|
|
842
|
+
return False
|
|
843
|
+
|
|
844
|
+
def restore_from_backup_metadata(self, backup_metadata: BackupMetadata) -> None:
|
|
845
|
+
self.console.print(
|
|
846
|
+
f"[yellow]🔄 Manually restoring from backup: {backup_metadata.backup_id}[/yellow]"
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
self.backup_service.restore_from_backup(backup_metadata, self.base_directory)
|
|
850
|
+
|
|
851
|
+
self.console.print(
|
|
852
|
+
f"[green]✅ Manual restoration completed from backup: "
|
|
853
|
+
f"{backup_metadata.backup_id}[/green]"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def create_emergency_backup(self, pkg_dir: Path | None = None) -> BackupMetadata:
|
|
857
|
+
validated_pkg_dir = self._prepare_package_directory(pkg_dir)
|
|
858
|
+
|
|
859
|
+
self.console.print(
|
|
860
|
+
"[cyan]🛡️ Creating emergency backup before risky operation...[/cyan]"
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
backup_metadata = self._create_backup(validated_pkg_dir)
|
|
864
|
+
|
|
865
|
+
self.console.print(
|
|
866
|
+
f"[green]✅ Emergency backup created: {backup_metadata.backup_id}[/green]"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
return backup_metadata
|
|
870
|
+
|
|
871
|
+
def restore_emergency_backup(self, backup_metadata: BackupMetadata) -> bool:
|
|
872
|
+
try:
|
|
873
|
+
self.console.print(
|
|
874
|
+
f"[yellow]🔄 Restoring emergency backup: {backup_metadata.backup_id}[/yellow]"
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
self.backup_service.restore_from_backup(
|
|
878
|
+
backup_metadata, self.base_directory
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
self.console.print(
|
|
882
|
+
f"[green]✅ Emergency backup restored successfully: {backup_metadata.backup_id}[/green]"
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
return True
|
|
886
|
+
|
|
887
|
+
except Exception as e:
|
|
888
|
+
self.logger.error(f"Emergency backup restoration failed: {e}")
|
|
889
|
+
self.console.print(
|
|
890
|
+
f"[red]💥 Emergency backup restoration failed: {e}[/red]\n"
|
|
891
|
+
f"[yellow]⚠️ Manual intervention required. Backup location: "
|
|
892
|
+
f"{backup_metadata.backup_directory}[/yellow]"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
return False
|
|
896
|
+
|
|
897
|
+
def verify_backup_integrity(self, backup_metadata: BackupMetadata) -> bool:
|
|
898
|
+
try:
|
|
899
|
+
validation_result = self.backup_service._validate_backup(backup_metadata)
|
|
900
|
+
|
|
901
|
+
if validation_result.is_valid:
|
|
902
|
+
self.console.print(
|
|
903
|
+
f"[green]✅ Backup verification passed: {backup_metadata.backup_id}[/green] "
|
|
904
|
+
f"({validation_result.total_validated} files verified)"
|
|
905
|
+
)
|
|
906
|
+
return True
|
|
907
|
+
else:
|
|
908
|
+
self.console.print(
|
|
909
|
+
f"[red]❌ Backup verification failed: {backup_metadata.backup_id}[/red]"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
for error in validation_result.validation_errors[:3]:
|
|
913
|
+
self.console.print(f"[red] • {error}[/red]")
|
|
914
|
+
|
|
915
|
+
if len(validation_result.validation_errors) > 3:
|
|
916
|
+
remaining = len(validation_result.validation_errors) - 3
|
|
917
|
+
self.console.print(f"[red] ... and {remaining} more errors[/red]")
|
|
918
|
+
|
|
919
|
+
return False
|
|
920
|
+
|
|
921
|
+
except Exception as e:
|
|
922
|
+
self.logger.error(f"Backup verification failed with exception: {e}")
|
|
923
|
+
self.console.print(f"[red]💥 Backup verification error: {e}[/red]")
|
|
924
|
+
return False
|
|
925
|
+
|
|
926
|
+
def list_available_backups(self) -> list[Path]:
|
|
927
|
+
if (
|
|
928
|
+
not self.backup_service.backup_root
|
|
929
|
+
or not self.backup_service.backup_root.exists()
|
|
930
|
+
):
|
|
931
|
+
self.console.print("[yellow]⚠️ No backup root directory found[/yellow]")
|
|
932
|
+
return []
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
backup_dirs = [
|
|
936
|
+
path
|
|
937
|
+
for path in self.backup_service.backup_root.iterdir()
|
|
938
|
+
if path.is_dir() and path.name.startswith("backup_")
|
|
939
|
+
]
|
|
940
|
+
|
|
941
|
+
if backup_dirs:
|
|
942
|
+
self.console.print(
|
|
943
|
+
f"[cyan]📦 Found {len(backup_dirs)} available backups: [/cyan]"
|
|
944
|
+
)
|
|
945
|
+
for backup_dir in sorted(backup_dirs):
|
|
946
|
+
self.console.print(f" • {backup_dir.name}")
|
|
947
|
+
else:
|
|
948
|
+
self.console.print("[yellow]⚠️ No backups found[/yellow]")
|
|
949
|
+
|
|
950
|
+
return backup_dirs
|
|
951
|
+
|
|
952
|
+
except Exception as e:
|
|
953
|
+
self.logger.error(f"Failed to list[t.Any] backups: {e}")
|
|
954
|
+
self.console.print(f"[red]💥 Error listing backups: {e}[/red]")
|
|
955
|
+
return []
|
|
956
|
+
|
|
957
|
+
def should_process_file(self, file_path: Path) -> bool:
|
|
958
|
+
try:
|
|
959
|
+
validated_path = SecurePathValidator.validate_file_path(
|
|
960
|
+
file_path, self.base_directory
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
SecurePathValidator.validate_file_size(validated_path)
|
|
964
|
+
|
|
965
|
+
ignore_patterns = {
|
|
966
|
+
"__pycache__",
|
|
967
|
+
".git",
|
|
968
|
+
".venv",
|
|
969
|
+
"site-packages",
|
|
970
|
+
".pytest_cache",
|
|
971
|
+
"build",
|
|
972
|
+
"dist",
|
|
973
|
+
"tests",
|
|
974
|
+
"test",
|
|
975
|
+
"examples",
|
|
976
|
+
"example",
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
for parent in validated_path.parents:
|
|
980
|
+
if parent.name in ignore_patterns:
|
|
981
|
+
return False
|
|
982
|
+
|
|
983
|
+
should_process = not (
|
|
984
|
+
validated_path.name.startswith(".") or validated_path.suffix != ".py"
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
if should_process:
|
|
988
|
+
self.security_logger.log_security_event(
|
|
989
|
+
SecurityEventType.FILE_CLEANED,
|
|
990
|
+
SecurityEventLevel.LOW,
|
|
991
|
+
f"File approved for processing: {validated_path}",
|
|
992
|
+
file_path=validated_path,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
return should_process
|
|
996
|
+
|
|
997
|
+
except ExecutionError as e:
|
|
998
|
+
self.security_logger.log_validation_failed(
|
|
999
|
+
"file_processing_check",
|
|
1000
|
+
file_path,
|
|
1001
|
+
f"File failed security validation: {e}",
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
return False
|
|
1005
|
+
|
|
1006
|
+
except Exception as e:
|
|
1007
|
+
self.logger.warning(f"Unexpected error checking file {file_path}: {e}")
|
|
1008
|
+
return False
|
|
1009
|
+
|
|
1010
|
+
def _create_line_comment_step(self) -> CleaningStepProtocol:
|
|
1011
|
+
return self._LineCommentStep()
|
|
1012
|
+
|
|
1013
|
+
def _create_docstring_step(self) -> CleaningStepProtocol:
|
|
1014
|
+
return self._DocstringStep()
|
|
1015
|
+
|
|
1016
|
+
class _DocstringStep:
|
|
1017
|
+
name = "remove_docstrings"
|
|
1018
|
+
|
|
1019
|
+
def _is_docstring_node(self, node: ast.AST) -> bool:
|
|
1020
|
+
body = getattr(node, "body", None)
|
|
1021
|
+
return (
|
|
1022
|
+
hasattr(node, "body")
|
|
1023
|
+
and body is not None
|
|
1024
|
+
and len(body) > 0
|
|
1025
|
+
and isinstance(body[0], ast.Expr)
|
|
1026
|
+
and isinstance(body[0].value, ast.Constant)
|
|
1027
|
+
and isinstance(body[0].value.value, str)
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
def _find_docstrings(self, tree: ast.AST) -> list[ast.AST]:
|
|
1031
|
+
docstring_nodes: list[ast.AST] = []
|
|
1032
|
+
finder = self._DocstringFinder(docstring_nodes, self._is_docstring_node)
|
|
1033
|
+
finder.visit(tree)
|
|
1034
|
+
return docstring_nodes
|
|
1035
|
+
|
|
1036
|
+
class _DocstringFinder(ast.NodeVisitor):
|
|
1037
|
+
def __init__(
|
|
1038
|
+
self,
|
|
1039
|
+
docstring_nodes: list[ast.AST],
|
|
1040
|
+
is_docstring_node: t.Callable[[ast.AST], bool],
|
|
1041
|
+
):
|
|
1042
|
+
self.docstring_nodes = docstring_nodes
|
|
1043
|
+
self.is_docstring_node = is_docstring_node
|
|
1044
|
+
|
|
1045
|
+
def _add_if_docstring(self, node: ast.AST) -> None:
|
|
1046
|
+
if self.is_docstring_node(node) and hasattr(node, "body"):
|
|
1047
|
+
body: list[ast.stmt] = getattr(node, "body")
|
|
1048
|
+
self.docstring_nodes.append(body[0])
|
|
1049
|
+
self.generic_visit(node)
|
|
1050
|
+
|
|
1051
|
+
def visit_Module(self, node: ast.Module) -> None:
|
|
1052
|
+
self._add_if_docstring(node)
|
|
1053
|
+
|
|
1054
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
1055
|
+
self._add_if_docstring(node)
|
|
1056
|
+
|
|
1057
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
1058
|
+
self._add_if_docstring(node)
|
|
1059
|
+
|
|
1060
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
1061
|
+
self._add_if_docstring(node)
|
|
1062
|
+
|
|
1063
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
1064
|
+
try:
|
|
1065
|
+
tree = ast.parse(code, filename=str(file_path))
|
|
1066
|
+
except SyntaxError:
|
|
1067
|
+
return self._regex_fallback_removal(code)
|
|
1068
|
+
|
|
1069
|
+
docstring_nodes = self._find_docstrings(tree)
|
|
1070
|
+
|
|
1071
|
+
if not docstring_nodes:
|
|
1072
|
+
return code
|
|
1073
|
+
|
|
1074
|
+
lines = code.split("\n")
|
|
1075
|
+
lines_to_remove: set[int] = set()
|
|
1076
|
+
|
|
1077
|
+
for node in docstring_nodes:
|
|
1078
|
+
start_line = getattr(node, "lineno", 1)
|
|
1079
|
+
end_line = getattr(node, "end_lineno", start_line)
|
|
1080
|
+
|
|
1081
|
+
lines_to_remove.update(range(start_line, end_line + 1))
|
|
1082
|
+
|
|
1083
|
+
result_lines = [
|
|
1084
|
+
line for i, line in enumerate(lines, 1) if i not in lines_to_remove
|
|
1085
|
+
]
|
|
1086
|
+
|
|
1087
|
+
result = "\n".join(result_lines)
|
|
1088
|
+
return self._regex_fallback_removal(result)
|
|
1089
|
+
|
|
1090
|
+
def _regex_fallback_removal(self, code: str) -> str:
|
|
1091
|
+
return _safe_applicator.apply_docstring_patterns(code)
|
|
1092
|
+
|
|
1093
|
+
class _LineCommentStep:
|
|
1094
|
+
name = "remove_line_comments"
|
|
1095
|
+
|
|
1096
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
1097
|
+
lines = code.split("\n")
|
|
1098
|
+
|
|
1099
|
+
processed_lines = [self._process_line_for_comments(line) for line in lines]
|
|
1100
|
+
return "\n".join(processed_lines)
|
|
1101
|
+
|
|
1102
|
+
def _process_line_for_comments(self, line: str) -> str:
|
|
1103
|
+
if not line.strip() or self._is_preserved_comment_line(line):
|
|
1104
|
+
return line
|
|
1105
|
+
return self._remove_comment_from_line(line)
|
|
1106
|
+
|
|
1107
|
+
def _is_preserved_comment_line(self, line: str) -> bool:
|
|
1108
|
+
stripped = line.strip()
|
|
1109
|
+
if not stripped.startswith("#"):
|
|
1110
|
+
return False
|
|
1111
|
+
return self._has_preserved_pattern(stripped)
|
|
1112
|
+
|
|
1113
|
+
def _has_preserved_pattern(self, stripped_line: str) -> bool:
|
|
1114
|
+
return _safe_applicator.has_preserved_comment(stripped_line)
|
|
1115
|
+
|
|
1116
|
+
def _remove_comment_from_line(self, line: str) -> str:
|
|
1117
|
+
"""Remove comment from line while preserving strings."""
|
|
1118
|
+
if not self._line_needs_comment_processing(line):
|
|
1119
|
+
return line
|
|
1120
|
+
|
|
1121
|
+
return self._process_line_for_comment_removal(line)
|
|
1122
|
+
|
|
1123
|
+
def _line_needs_comment_processing(self, line: str) -> bool:
|
|
1124
|
+
"""Check if line needs comment processing."""
|
|
1125
|
+
return '"' in line or "'" in line or "#" in line
|
|
1126
|
+
|
|
1127
|
+
def _process_line_for_comment_removal(self, line: str) -> str:
|
|
1128
|
+
"""Process line to remove comments while preserving strings and special comments."""
|
|
1129
|
+
result_chars = []
|
|
1130
|
+
string_state = {"in_string": False, "quote_char": None}
|
|
1131
|
+
|
|
1132
|
+
for i, char in enumerate(line):
|
|
1133
|
+
should_stop, preserve_rest = self._should_stop_for_comment(
|
|
1134
|
+
char, string_state, line, i
|
|
1135
|
+
)
|
|
1136
|
+
if should_stop:
|
|
1137
|
+
if preserve_rest:
|
|
1138
|
+
# Preserve the rest of the line (special comment like nosec, noqa, type:)
|
|
1139
|
+
result_chars.extend(line[i:])
|
|
1140
|
+
break
|
|
1141
|
+
|
|
1142
|
+
self._update_string_state(char, i, line, string_state)
|
|
1143
|
+
result_chars.append(char)
|
|
1144
|
+
|
|
1145
|
+
return "".join(result_chars).rstrip()
|
|
1146
|
+
|
|
1147
|
+
def _should_stop_for_comment(
|
|
1148
|
+
self, char: str, string_state: dict[str, t.Any], line: str, index: int
|
|
1149
|
+
) -> tuple[bool, bool]:
|
|
1150
|
+
"""Check if we should stop processing at comment.
|
|
1151
|
+
|
|
1152
|
+
Returns:
|
|
1153
|
+
(should_stop, preserve_rest): should_stop=True when comment found,
|
|
1154
|
+
preserve_rest=True if comment should be kept
|
|
1155
|
+
"""
|
|
1156
|
+
if string_state["in_string"] or char != "#":
|
|
1157
|
+
return (False, False)
|
|
1158
|
+
|
|
1159
|
+
# Check if the comment portion should be preserved
|
|
1160
|
+
comment_part = line[index:].strip()
|
|
1161
|
+
if _safe_applicator.has_preserved_comment(comment_part):
|
|
1162
|
+
return (True, True) # Stop processing, preserve the comment
|
|
1163
|
+
|
|
1164
|
+
return (True, False) # Stop processing, discard the comment
|
|
1165
|
+
|
|
1166
|
+
def _update_string_state(
|
|
1167
|
+
self, char: str, index: int, line: str, string_state: dict[str, t.Any]
|
|
1168
|
+
) -> None:
|
|
1169
|
+
"""Update string parsing state."""
|
|
1170
|
+
if not string_state["in_string"]:
|
|
1171
|
+
if char in ('"', "'"):
|
|
1172
|
+
string_state["in_string"] = True
|
|
1173
|
+
string_state["quote_char"] = char
|
|
1174
|
+
elif char == string_state["quote_char"] and (
|
|
1175
|
+
index == 0 or line[index - 1] != "\\"
|
|
1176
|
+
):
|
|
1177
|
+
string_state["in_string"] = False
|
|
1178
|
+
string_state["quote_char"] = None
|
|
1179
|
+
|
|
1180
|
+
def _create_docstring_finder_class(
|
|
1181
|
+
self,
|
|
1182
|
+
docstring_nodes: list[ast.AST],
|
|
1183
|
+
) -> type[ast.NodeVisitor]:
|
|
1184
|
+
class DocstringFinder(ast.NodeVisitor):
|
|
1185
|
+
def _is_docstring_node(self, node: ast.AST) -> bool:
|
|
1186
|
+
body = getattr(node, "body", None)
|
|
1187
|
+
return (
|
|
1188
|
+
hasattr(node, "body")
|
|
1189
|
+
and body is not None
|
|
1190
|
+
and len(body) > 0
|
|
1191
|
+
and isinstance(body[0], ast.Expr)
|
|
1192
|
+
and isinstance(body[0].value, ast.Constant)
|
|
1193
|
+
and isinstance(body[0].value.value, str)
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
def _add_if_docstring(self, node: ast.AST) -> None:
|
|
1197
|
+
if self._is_docstring_node(node) and hasattr(node, "body"):
|
|
1198
|
+
body: list[ast.stmt] = getattr(node, "body")
|
|
1199
|
+
docstring_nodes.append(body[0])
|
|
1200
|
+
self.generic_visit(node)
|
|
1201
|
+
|
|
1202
|
+
def visit_Module(self, node: ast.Module) -> None:
|
|
1203
|
+
self._add_if_docstring(node)
|
|
1204
|
+
|
|
1205
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
1206
|
+
self._add_if_docstring(node)
|
|
1207
|
+
|
|
1208
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
1209
|
+
self._add_if_docstring(node)
|
|
1210
|
+
|
|
1211
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
1212
|
+
self._add_if_docstring(node)
|
|
1213
|
+
|
|
1214
|
+
return DocstringFinder
|
|
1215
|
+
|
|
1216
|
+
def _create_whitespace_step(self) -> CleaningStepProtocol:
|
|
1217
|
+
class WhitespaceStep:
|
|
1218
|
+
name = "remove_extra_whitespace"
|
|
1219
|
+
|
|
1220
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
1221
|
+
lines = code.split("\n")
|
|
1222
|
+
cleaned_lines: list[str] = []
|
|
1223
|
+
empty_line_count = 0
|
|
1224
|
+
|
|
1225
|
+
for line in lines:
|
|
1226
|
+
cleaned_line = line.rstrip()
|
|
1227
|
+
|
|
1228
|
+
if not cleaned_line.strip():
|
|
1229
|
+
empty_line_count += 1
|
|
1230
|
+
if empty_line_count <= 2:
|
|
1231
|
+
cleaned_lines.append("")
|
|
1232
|
+
else:
|
|
1233
|
+
empty_line_count = 0
|
|
1234
|
+
leading_whitespace = len(cleaned_line) - len(
|
|
1235
|
+
cleaned_line.lstrip()
|
|
1236
|
+
)
|
|
1237
|
+
content = cleaned_line.lstrip()
|
|
1238
|
+
|
|
1239
|
+
content = SAFE_PATTERNS["multiple_spaces"].apply(content)
|
|
1240
|
+
|
|
1241
|
+
cleaned_line = cleaned_line[:leading_whitespace] + content
|
|
1242
|
+
cleaned_lines.append(cleaned_line)
|
|
1243
|
+
|
|
1244
|
+
while cleaned_lines and not cleaned_lines[-1].strip():
|
|
1245
|
+
cleaned_lines.pop()
|
|
1246
|
+
|
|
1247
|
+
result = "\n".join(cleaned_lines)
|
|
1248
|
+
if result and not result.endswith("\n"):
|
|
1249
|
+
result += "\n"
|
|
1250
|
+
|
|
1251
|
+
return result
|
|
1252
|
+
|
|
1253
|
+
return WhitespaceStep()
|
|
1254
|
+
|
|
1255
|
+
def _create_formatting_step(self) -> CleaningStepProtocol:
|
|
1256
|
+
class FormattingStep:
|
|
1257
|
+
name = "format_code"
|
|
1258
|
+
|
|
1259
|
+
def _is_preserved_comment_line(self, line: str) -> bool:
|
|
1260
|
+
stripped = line.strip()
|
|
1261
|
+
if not stripped.startswith("#"):
|
|
1262
|
+
return False
|
|
1263
|
+
return _safe_applicator.has_preserved_comment(line)
|
|
1264
|
+
|
|
1265
|
+
def __call__(self, code: str, file_path: Path) -> str:
|
|
1266
|
+
lines = code.split("\n")
|
|
1267
|
+
formatted_lines: list[str] = []
|
|
1268
|
+
|
|
1269
|
+
for line in lines:
|
|
1270
|
+
if line.strip():
|
|
1271
|
+
if self._is_preserved_comment_line(line):
|
|
1272
|
+
formatted_lines.append(line)
|
|
1273
|
+
continue
|
|
1274
|
+
|
|
1275
|
+
leading_whitespace = len(line) - len(line.lstrip())
|
|
1276
|
+
content = line.lstrip()
|
|
1277
|
+
|
|
1278
|
+
content = _safe_applicator.apply_formatting_patterns(content)
|
|
1279
|
+
|
|
1280
|
+
formatted_line = line[:leading_whitespace] + content
|
|
1281
|
+
formatted_lines.append(formatted_line)
|
|
1282
|
+
else:
|
|
1283
|
+
formatted_lines.append(line)
|
|
1284
|
+
|
|
1285
|
+
return "\n".join(formatted_lines)
|
|
1286
|
+
|
|
1287
|
+
return FormattingStep()
|
|
1288
|
+
|
|
1289
|
+
def remove_line_comments(self, code: str, file_path: Path | None = None) -> str:
|
|
1290
|
+
file_path = file_path or Path("temp.py")
|
|
1291
|
+
step = self._create_line_comment_step()
|
|
1292
|
+
return step(code, file_path)
|
|
1293
|
+
|
|
1294
|
+
def remove_docstrings(self, code: str, file_path: Path | None = None) -> str:
|
|
1295
|
+
file_path = file_path or Path("temp.py")
|
|
1296
|
+
step = self._create_docstring_step()
|
|
1297
|
+
return step(code, file_path)
|
|
1298
|
+
|
|
1299
|
+
def remove_extra_whitespace(self, code: str, file_path: Path | None = None) -> str:
|
|
1300
|
+
file_path = file_path or Path("temp.py")
|
|
1301
|
+
step = self._create_whitespace_step()
|
|
1302
|
+
return step(code, file_path)
|
|
1303
|
+
|
|
1304
|
+
def format_code(self, code: str, file_path: Path | None = None) -> str:
|
|
1305
|
+
file_path = file_path or Path("temp.py")
|
|
1306
|
+
step = self._create_formatting_step()
|
|
1307
|
+
return step(code, file_path)
|