crackerjack 0.31.10__py3-none-any.whl → 0.31.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -25,12 +25,21 @@ class AsyncHookManager:
|
|
|
25
25
|
quiet=True,
|
|
26
26
|
)
|
|
27
27
|
self.config_loader = HookConfigLoader()
|
|
28
|
+
self._config_path: Path | None = None
|
|
29
|
+
|
|
30
|
+
def set_config_path(self, config_path: Path) -> None:
|
|
31
|
+
"""Set the path to the pre-commit configuration file."""
|
|
32
|
+
self._config_path = config_path
|
|
28
33
|
|
|
29
34
|
async def run_fast_hooks_async(self) -> list[HookResult]:
|
|
30
35
|
strategy = self.config_loader.load_strategy("fast")
|
|
31
36
|
|
|
32
37
|
strategy.parallel = False
|
|
33
38
|
|
|
39
|
+
if self._config_path:
|
|
40
|
+
for hook in strategy.hooks:
|
|
41
|
+
hook.config_path = self._config_path
|
|
42
|
+
|
|
34
43
|
execution_result = await self.async_executor.execute_strategy(strategy)
|
|
35
44
|
return execution_result.results
|
|
36
45
|
|
|
@@ -40,6 +49,10 @@ class AsyncHookManager:
|
|
|
40
49
|
strategy.parallel = True
|
|
41
50
|
strategy.max_workers = 3
|
|
42
51
|
|
|
52
|
+
if self._config_path:
|
|
53
|
+
for hook in strategy.hooks:
|
|
54
|
+
hook.config_path = self._config_path
|
|
55
|
+
|
|
43
56
|
execution_result = await self.async_executor.execute_strategy(strategy)
|
|
44
57
|
return execution_result.results
|
|
45
58
|
|
|
@@ -52,6 +65,8 @@ class AsyncHookManager:
|
|
|
52
65
|
async def install_hooks_async(self) -> bool:
|
|
53
66
|
try:
|
|
54
67
|
process = await asyncio.create_subprocess_exec(
|
|
68
|
+
"uv",
|
|
69
|
+
"run",
|
|
55
70
|
"pre-commit",
|
|
56
71
|
"install",
|
|
57
72
|
cwd=self.pkg_path,
|
|
@@ -62,19 +77,19 @@ class AsyncHookManager:
|
|
|
62
77
|
_, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
|
|
63
78
|
|
|
64
79
|
if process.returncode == 0:
|
|
65
|
-
self.console.print("[green]✅[/green] Pre-commit hooks installed")
|
|
80
|
+
self.console.print("[green]✅[/ green] Pre-commit hooks installed")
|
|
66
81
|
return True
|
|
67
82
|
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
68
83
|
self.console.print(
|
|
69
|
-
f"[red]❌[/red] Failed to install hooks: {error_msg}",
|
|
84
|
+
f"[red]❌[/ red] Failed to install hooks: {error_msg}",
|
|
70
85
|
)
|
|
71
86
|
return False
|
|
72
87
|
|
|
73
88
|
except TimeoutError:
|
|
74
|
-
self.console.print("[red]❌[/red] Hook installation timed out")
|
|
89
|
+
self.console.print("[red]❌[/ red] Hook installation timed out")
|
|
75
90
|
return False
|
|
76
91
|
except Exception as e:
|
|
77
|
-
self.console.print(f"[red]❌[/red] Error installing hooks: {e}")
|
|
92
|
+
self.console.print(f"[red]❌[/ red] Error installing hooks: {e}")
|
|
78
93
|
return False
|
|
79
94
|
|
|
80
95
|
def install_hooks(self) -> bool:
|
|
@@ -83,6 +98,8 @@ class AsyncHookManager:
|
|
|
83
98
|
async def update_hooks_async(self) -> bool:
|
|
84
99
|
try:
|
|
85
100
|
process = await asyncio.create_subprocess_exec(
|
|
101
|
+
"uv",
|
|
102
|
+
"run",
|
|
86
103
|
"pre-commit",
|
|
87
104
|
"autoupdate",
|
|
88
105
|
cwd=self.pkg_path,
|
|
@@ -93,17 +110,17 @@ class AsyncHookManager:
|
|
|
93
110
|
_, stderr = await asyncio.wait_for(process.communicate(), timeout=60)
|
|
94
111
|
|
|
95
112
|
if process.returncode == 0:
|
|
96
|
-
self.console.print("[green]✅[/green] Pre-commit hooks updated")
|
|
113
|
+
self.console.print("[green]✅[/ green] Pre-commit hooks updated")
|
|
97
114
|
return True
|
|
98
115
|
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
99
|
-
self.console.print(f"[red]❌[/red] Failed to update hooks: {error_msg}")
|
|
116
|
+
self.console.print(f"[red]❌[/ red] Failed to update hooks: {error_msg}")
|
|
100
117
|
return False
|
|
101
118
|
|
|
102
119
|
except TimeoutError:
|
|
103
|
-
self.console.print("[red]❌[/red] Hook update timed out")
|
|
120
|
+
self.console.print("[red]❌[/ red] Hook update timed out")
|
|
104
121
|
return False
|
|
105
122
|
except Exception as e:
|
|
106
|
-
self.console.print(f"[red]❌[/red] Error updating hooks: {e}")
|
|
123
|
+
self.console.print(f"[red]❌[/ red] Error updating hooks: {e}")
|
|
107
124
|
return False
|
|
108
125
|
|
|
109
126
|
def update_hooks(self) -> bool:
|
|
@@ -52,7 +52,7 @@ class HookManagerImpl:
|
|
|
52
52
|
def validate_hooks_config(self) -> bool:
|
|
53
53
|
try:
|
|
54
54
|
result = subprocess.run(
|
|
55
|
-
["pre-commit", "validate-config"],
|
|
55
|
+
["uv", "run", "pre-commit", "validate-config"],
|
|
56
56
|
cwd=self.pkg_path,
|
|
57
57
|
capture_output=True,
|
|
58
58
|
text=True,
|
|
@@ -72,7 +72,7 @@ class HookManagerImpl:
|
|
|
72
72
|
def install_hooks(self) -> bool:
|
|
73
73
|
try:
|
|
74
74
|
result = subprocess.run(
|
|
75
|
-
["pre-commit", "install"],
|
|
75
|
+
["uv", "run", "pre-commit", "install"],
|
|
76
76
|
check=False,
|
|
77
77
|
cwd=self.pkg_path,
|
|
78
78
|
capture_output=True,
|
|
@@ -80,20 +80,20 @@ class HookManagerImpl:
|
|
|
80
80
|
timeout=30,
|
|
81
81
|
)
|
|
82
82
|
if result.returncode == 0:
|
|
83
|
-
self.console.print("[green]✅[/green] Pre-commit hooks installed")
|
|
83
|
+
self.console.print("[green]✅[/ green] Pre-commit hooks installed")
|
|
84
84
|
return True
|
|
85
85
|
self.console.print(
|
|
86
|
-
f"[red]❌[/red] Failed to install hooks: {result.stderr}",
|
|
86
|
+
f"[red]❌[/ red] Failed to install hooks: {result.stderr}",
|
|
87
87
|
)
|
|
88
88
|
return False
|
|
89
89
|
except Exception as e:
|
|
90
|
-
self.console.print(f"[red]❌[/red] Error installing hooks: {e}")
|
|
90
|
+
self.console.print(f"[red]❌[/ red] Error installing hooks: {e}")
|
|
91
91
|
return False
|
|
92
92
|
|
|
93
93
|
def update_hooks(self) -> bool:
|
|
94
94
|
try:
|
|
95
95
|
result = subprocess.run(
|
|
96
|
-
["pre-commit", "autoupdate"],
|
|
96
|
+
["uv", "run", "pre-commit", "autoupdate"],
|
|
97
97
|
check=False,
|
|
98
98
|
cwd=self.pkg_path,
|
|
99
99
|
capture_output=True,
|
|
@@ -101,14 +101,14 @@ class HookManagerImpl:
|
|
|
101
101
|
timeout=60,
|
|
102
102
|
)
|
|
103
103
|
if result.returncode == 0:
|
|
104
|
-
self.console.print("[green]✅[/green] Pre-commit hooks updated")
|
|
104
|
+
self.console.print("[green]✅[/ green] Pre-commit hooks updated")
|
|
105
105
|
return True
|
|
106
106
|
self.console.print(
|
|
107
|
-
f"[red]❌[/red] Failed to update hooks: {result.stderr}",
|
|
107
|
+
f"[red]❌[/ red] Failed to update hooks: {result.stderr}",
|
|
108
108
|
)
|
|
109
109
|
return False
|
|
110
110
|
except Exception as e:
|
|
111
|
-
self.console.print(f"[red]❌[/red] Error updating hooks: {e}")
|
|
111
|
+
self.console.print(f"[red]❌[/ red] Error updating hooks: {e}")
|
|
112
112
|
return False
|
|
113
113
|
|
|
114
114
|
def get_hook_summary(self, results: list[HookResult]) -> dict[str, t.Any]:
|
|
@@ -52,32 +52,29 @@ class PublishManagerImpl:
|
|
|
52
52
|
data = loads(content)
|
|
53
53
|
return data.get("project", {}).get("version")
|
|
54
54
|
except Exception as e:
|
|
55
|
-
self.console.print(f"[yellow]⚠️[/yellow] Error reading version: {e}")
|
|
55
|
+
self.console.print(f"[yellow]⚠️[/ yellow] Error reading version: {e}")
|
|
56
56
|
return None
|
|
57
57
|
|
|
58
58
|
def _update_version_in_file(self, new_version: str) -> bool:
|
|
59
59
|
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
60
60
|
try:
|
|
61
61
|
content = self.filesystem.read_file(pyproject_path)
|
|
62
|
-
import
|
|
62
|
+
from crackerjack.services.regex_patterns import update_pyproject_version
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
pattern = r'^(version\s*=\s*["\'])([^"\']+)(["\'])$'
|
|
66
|
-
replacement = f"\\g<1>{new_version}\\g<3>"
|
|
67
|
-
new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
|
|
64
|
+
new_content = update_pyproject_version(content, new_version)
|
|
68
65
|
if content != new_content:
|
|
69
66
|
if not self.dry_run:
|
|
70
67
|
self.filesystem.write_file(pyproject_path, new_content)
|
|
71
68
|
self.console.print(
|
|
72
|
-
f"[green]✅[/green] Updated version to {new_version}",
|
|
69
|
+
f"[green]✅[/ green] Updated version to {new_version}",
|
|
73
70
|
)
|
|
74
71
|
return True
|
|
75
72
|
self.console.print(
|
|
76
|
-
"[yellow]⚠️[/yellow] Version pattern not found in pyproject.toml",
|
|
73
|
+
"[yellow]⚠️[/ yellow] Version pattern not found in pyproject.toml",
|
|
77
74
|
)
|
|
78
75
|
return False
|
|
79
76
|
except Exception as e:
|
|
80
|
-
self.console.print(f"[red]❌[/red] Error updating version: {e}")
|
|
77
|
+
self.console.print(f"[red]❌[/ red] Error updating version: {e}")
|
|
81
78
|
return False
|
|
82
79
|
|
|
83
80
|
def _calculate_next_version(self, current: str, bump_type: str) -> str:
|
|
@@ -96,18 +93,17 @@ class PublishManagerImpl:
|
|
|
96
93
|
msg = f"Invalid bump type: {bump_type}"
|
|
97
94
|
raise ValueError(msg)
|
|
98
95
|
except Exception as e:
|
|
99
|
-
self.console.print(f"[red]❌[/red] Error calculating version: {e}")
|
|
96
|
+
self.console.print(f"[red]❌[/ red] Error calculating version: {e}")
|
|
100
97
|
raise
|
|
101
98
|
|
|
102
99
|
def bump_version(self, version_type: str) -> str:
|
|
103
100
|
current_version = self._get_current_version()
|
|
104
101
|
if not current_version:
|
|
105
|
-
self.console.print("[red]❌[/red] Could not determine current version")
|
|
102
|
+
self.console.print("[red]❌[/ red] Could not determine current version")
|
|
106
103
|
msg = "Cannot determine current version"
|
|
107
104
|
raise ValueError(msg)
|
|
108
|
-
self.console.print(f"[cyan]📦[/cyan] Current version: {current_version}")
|
|
105
|
+
self.console.print(f"[cyan]📦[/ cyan] Current version: {current_version}")
|
|
109
106
|
|
|
110
|
-
# Handle interactive version selection
|
|
111
107
|
if version_type == "interactive":
|
|
112
108
|
version_type = self._prompt_for_version_type()
|
|
113
109
|
|
|
@@ -115,11 +111,11 @@ class PublishManagerImpl:
|
|
|
115
111
|
new_version = self._calculate_next_version(current_version, version_type)
|
|
116
112
|
if self.dry_run:
|
|
117
113
|
self.console.print(
|
|
118
|
-
f"[yellow]🔍[/yellow] Would bump {version_type} version: {current_version} → {new_version}",
|
|
114
|
+
f"[yellow]🔍[/ yellow] Would bump {version_type} version: {current_version} → {new_version}",
|
|
119
115
|
)
|
|
120
116
|
elif self._update_version_in_file(new_version):
|
|
121
117
|
self.console.print(
|
|
122
|
-
f"[green]🚀[/green] Bumped {version_type} version: {current_version} → {new_version}",
|
|
118
|
+
f"[green]🚀[/ green] Bumped {version_type} version: {current_version} → {new_version}",
|
|
123
119
|
)
|
|
124
120
|
else:
|
|
125
121
|
msg = "Failed to update version in file"
|
|
@@ -127,22 +123,21 @@ class PublishManagerImpl:
|
|
|
127
123
|
|
|
128
124
|
return new_version
|
|
129
125
|
except Exception as e:
|
|
130
|
-
self.console.print(f"[red]❌[/red] Version bump failed: {e}")
|
|
126
|
+
self.console.print(f"[red]❌[/ red] Version bump failed: {e}")
|
|
131
127
|
raise
|
|
132
128
|
|
|
133
129
|
def _prompt_for_version_type(self) -> str:
|
|
134
|
-
"""Prompt user to select version type interactively."""
|
|
135
130
|
try:
|
|
136
131
|
from rich.prompt import Prompt
|
|
137
132
|
|
|
138
133
|
return Prompt.ask(
|
|
139
|
-
"[cyan]📦[/cyan] Select version bump type",
|
|
134
|
+
"[cyan]📦[/ cyan] Select version bump type",
|
|
140
135
|
choices=["patch", "minor", "major"],
|
|
141
136
|
default="patch",
|
|
142
137
|
)
|
|
143
138
|
except ImportError:
|
|
144
139
|
self.console.print(
|
|
145
|
-
"[yellow]⚠️[/yellow] Rich prompt not available, defaulting to patch"
|
|
140
|
+
"[yellow]⚠️[/ yellow] Rich prompt not available, defaulting to patch"
|
|
146
141
|
)
|
|
147
142
|
return "patch"
|
|
148
143
|
|
|
@@ -172,24 +167,29 @@ class PublishManagerImpl:
|
|
|
172
167
|
|
|
173
168
|
if self.security.validate_token_format(token, "pypi"):
|
|
174
169
|
masked_token = self.security.mask_tokens(token)
|
|
175
|
-
self.console.print(f"[dim]Token format: {masked_token}[/dim]", style="dim")
|
|
170
|
+
self.console.print(f"[dim]Token format: {masked_token}[/ dim]", style="dim")
|
|
176
171
|
return "Environment variable (UV_PUBLISH_TOKEN)"
|
|
177
172
|
self.console.print(
|
|
178
|
-
"[yellow]⚠️[/yellow] UV_PUBLISH_TOKEN format appears invalid",
|
|
173
|
+
"[yellow]⚠️[/ yellow] UV_PUBLISH_TOKEN format appears invalid",
|
|
179
174
|
)
|
|
180
175
|
return None
|
|
181
176
|
|
|
182
177
|
def _check_keyring_auth(self) -> str | None:
|
|
183
178
|
try:
|
|
184
179
|
result = self._run_command(
|
|
185
|
-
[
|
|
180
|
+
[
|
|
181
|
+
"keyring",
|
|
182
|
+
"get",
|
|
183
|
+
"https://upload.pypi.org/legacy/",
|
|
184
|
+
"__token__",
|
|
185
|
+
],
|
|
186
186
|
)
|
|
187
187
|
if result.returncode == 0 and result.stdout.strip():
|
|
188
188
|
keyring_token = result.stdout.strip()
|
|
189
189
|
if self.security.validate_token_format(keyring_token, "pypi"):
|
|
190
190
|
return "Keyring storage"
|
|
191
191
|
self.console.print(
|
|
192
|
-
"[yellow]⚠️[/yellow] Keyring token format appears invalid",
|
|
192
|
+
"[yellow]⚠️[/ yellow] Keyring token format appears invalid",
|
|
193
193
|
)
|
|
194
194
|
except (subprocess.SubprocessError, OSError, FileNotFoundError):
|
|
195
195
|
pass
|
|
@@ -197,16 +197,16 @@ class PublishManagerImpl:
|
|
|
197
197
|
|
|
198
198
|
def _report_auth_status(self, auth_methods: list[str]) -> bool:
|
|
199
199
|
if auth_methods:
|
|
200
|
-
self.console.print("[green]✅[/green] PyPI authentication available: ")
|
|
200
|
+
self.console.print("[green]✅[/ green] PyPI authentication available: ")
|
|
201
201
|
for method in auth_methods:
|
|
202
|
-
self.console.print(f"
|
|
202
|
+
self.console.print(f"-{method}")
|
|
203
203
|
return True
|
|
204
204
|
self._display_auth_setup_instructions()
|
|
205
205
|
return False
|
|
206
206
|
|
|
207
207
|
def _display_auth_setup_instructions(self) -> None:
|
|
208
|
-
self.console.print("[red]❌[/red] No valid PyPI authentication found")
|
|
209
|
-
self.console.print("\n[yellow]💡[/yellow] Setup options: ")
|
|
208
|
+
self.console.print("[red]❌[/ red] No valid PyPI authentication found")
|
|
209
|
+
self.console.print("\n[yellow]💡[/ yellow] Setup options: ")
|
|
210
210
|
self.console.print(
|
|
211
211
|
" 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
|
|
212
212
|
)
|
|
@@ -219,22 +219,21 @@ class PublishManagerImpl:
|
|
|
219
219
|
|
|
220
220
|
def build_package(self) -> bool:
|
|
221
221
|
try:
|
|
222
|
-
self.console.print("[yellow]🔨[/yellow] Building package...")
|
|
222
|
+
self.console.print("[yellow]🔨[/ yellow] Building package...")
|
|
223
223
|
|
|
224
224
|
if self.dry_run:
|
|
225
225
|
return self._handle_dry_run_build()
|
|
226
226
|
|
|
227
227
|
return self._execute_build()
|
|
228
228
|
except Exception as e:
|
|
229
|
-
self.console.print(f"[red]❌[/red] Build error: {e}")
|
|
229
|
+
self.console.print(f"[red]❌[/ red] Build error: {e}")
|
|
230
230
|
return False
|
|
231
231
|
|
|
232
232
|
def _handle_dry_run_build(self) -> bool:
|
|
233
|
-
self.console.print("[yellow]🔍[/yellow] Would build package")
|
|
233
|
+
self.console.print("[yellow]🔍[/ yellow] Would build package")
|
|
234
234
|
return True
|
|
235
235
|
|
|
236
236
|
def _clean_dist_directory(self) -> None:
|
|
237
|
-
"""Clean dist directory to ensure only current version artifacts are uploaded."""
|
|
238
237
|
dist_dir = self.pkg_path / "dist"
|
|
239
238
|
if not dist_dir.exists():
|
|
240
239
|
return
|
|
@@ -242,27 +241,26 @@ class PublishManagerImpl:
|
|
|
242
241
|
try:
|
|
243
242
|
import shutil
|
|
244
243
|
|
|
245
|
-
# Remove entire dist directory and recreate it
|
|
246
244
|
shutil.rmtree(dist_dir)
|
|
247
245
|
dist_dir.mkdir(exist_ok=True)
|
|
248
|
-
self.console.print(
|
|
246
|
+
self.console.print(
|
|
247
|
+
"[cyan]🧹[/ cyan] Cleaned dist directory for fresh build"
|
|
248
|
+
)
|
|
249
249
|
except Exception as e:
|
|
250
250
|
self.console.print(
|
|
251
|
-
f"[yellow]⚠️[/yellow] Warning: Could not clean dist directory: {e}"
|
|
251
|
+
f"[yellow]⚠️[/ yellow] Warning: Could not clean dist directory: {e}"
|
|
252
252
|
)
|
|
253
|
-
# Continue with build anyway - uv publish will fail with clear error
|
|
254
253
|
|
|
255
254
|
def _execute_build(self) -> bool:
|
|
256
|
-
# Clean dist directory before building to avoid uploading multiple versions
|
|
257
255
|
self._clean_dist_directory()
|
|
258
256
|
|
|
259
257
|
result = self._run_command(["uv", "build"])
|
|
260
258
|
|
|
261
259
|
if result.returncode != 0:
|
|
262
|
-
self.console.print(f"[red]❌[/red] Build failed: {result.stderr}")
|
|
260
|
+
self.console.print(f"[red]❌[/ red] Build failed: {result.stderr}")
|
|
263
261
|
return False
|
|
264
262
|
|
|
265
|
-
self.console.print("[green]✅[/green] Package built successfully")
|
|
263
|
+
self.console.print("[green]✅[/ green] Package built successfully")
|
|
266
264
|
self._display_build_artifacts()
|
|
267
265
|
return True
|
|
268
266
|
|
|
@@ -272,11 +270,11 @@ class PublishManagerImpl:
|
|
|
272
270
|
return
|
|
273
271
|
|
|
274
272
|
artifacts = list(dist_dir.glob("*"))
|
|
275
|
-
self.console.print(f"[cyan]📦[/cyan] Build artifacts ({len(artifacts)}): ")
|
|
273
|
+
self.console.print(f"[cyan]📦[/ cyan] Build artifacts ({len(artifacts)}): ")
|
|
276
274
|
|
|
277
275
|
for artifact in artifacts[-5:]:
|
|
278
276
|
size_str = self._format_file_size(artifact.stat().st_size)
|
|
279
|
-
self.console.print(f"
|
|
277
|
+
self.console.print(f"-{artifact.name} ({size_str})")
|
|
280
278
|
|
|
281
279
|
def _format_file_size(self, size: int) -> str:
|
|
282
280
|
if size < 1024 * 1024:
|
|
@@ -288,10 +286,10 @@ class PublishManagerImpl:
|
|
|
288
286
|
return False
|
|
289
287
|
|
|
290
288
|
try:
|
|
291
|
-
self.console.print("[yellow]🚀[/yellow] Publishing to PyPI...")
|
|
289
|
+
self.console.print("[yellow]🚀[/ yellow] Publishing to PyPI...")
|
|
292
290
|
return self._perform_publish_workflow()
|
|
293
291
|
except Exception as e:
|
|
294
|
-
self.console.print(f"[red]❌[/red] Publish error: {e}")
|
|
292
|
+
self.console.print(f"[red]❌[/ red] Publish error: {e}")
|
|
295
293
|
return False
|
|
296
294
|
|
|
297
295
|
def _validate_prerequisites(self) -> bool:
|
|
@@ -307,7 +305,7 @@ class PublishManagerImpl:
|
|
|
307
305
|
return self._execute_publish()
|
|
308
306
|
|
|
309
307
|
def _handle_dry_run_publish(self) -> bool:
|
|
310
|
-
self.console.print("[yellow]🔍[/yellow] Would publish package to PyPI")
|
|
308
|
+
self.console.print("[yellow]🔍[/ yellow] Would publish package to PyPI")
|
|
311
309
|
return True
|
|
312
310
|
|
|
313
311
|
def _execute_publish(self) -> bool:
|
|
@@ -321,10 +319,10 @@ class PublishManagerImpl:
|
|
|
321
319
|
return True
|
|
322
320
|
|
|
323
321
|
def _handle_publish_failure(self, error_msg: str) -> None:
|
|
324
|
-
self.console.print(f"[red]❌[/red] Publish failed: {error_msg}")
|
|
322
|
+
self.console.print(f"[red]❌[/ red] Publish failed: {error_msg}")
|
|
325
323
|
|
|
326
324
|
def _handle_publish_success(self) -> None:
|
|
327
|
-
self.console.print("[green]🎉[/green] Package published successfully!")
|
|
325
|
+
self.console.print("[green]🎉[/ green] Package published successfully !")
|
|
328
326
|
self._display_package_url()
|
|
329
327
|
|
|
330
328
|
def _display_package_url(self) -> None:
|
|
@@ -333,7 +331,7 @@ class PublishManagerImpl:
|
|
|
333
331
|
|
|
334
332
|
if package_name and current_version:
|
|
335
333
|
url = f"https://pypi.org/project/{package_name}/{current_version}/"
|
|
336
|
-
self.console.print(f"[cyan]🔗[/cyan] Package URL: {url}")
|
|
334
|
+
self.console.print(f"[cyan]🔗[/ cyan] Package URL: {url}")
|
|
337
335
|
|
|
338
336
|
def _get_package_name(self) -> str | None:
|
|
339
337
|
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
@@ -350,11 +348,11 @@ class PublishManagerImpl:
|
|
|
350
348
|
def cleanup_old_releases(self, keep_releases: int = 10) -> bool:
|
|
351
349
|
try:
|
|
352
350
|
self.console.print(
|
|
353
|
-
f"[yellow]🧹[/yellow] Cleaning up old releases (keeping {keep_releases})...",
|
|
351
|
+
f"[yellow]🧹[/ yellow] Cleaning up old releases (keeping {keep_releases})...",
|
|
354
352
|
)
|
|
355
353
|
if self.dry_run:
|
|
356
354
|
self.console.print(
|
|
357
|
-
"[yellow]🔍[/yellow] Would clean up old PyPI releases",
|
|
355
|
+
"[yellow]🔍[/ yellow] Would clean up old PyPI releases",
|
|
358
356
|
)
|
|
359
357
|
return True
|
|
360
358
|
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
@@ -365,48 +363,48 @@ class PublishManagerImpl:
|
|
|
365
363
|
package_name = data.get("project", {}).get("name", "")
|
|
366
364
|
if not package_name:
|
|
367
365
|
self.console.print(
|
|
368
|
-
"[yellow]⚠️[/yellow] Could not determine package name",
|
|
366
|
+
"[yellow]⚠️[/ yellow] Could not determine package name",
|
|
369
367
|
)
|
|
370
368
|
return False
|
|
371
369
|
self.console.print(
|
|
372
|
-
f"[cyan]📦[/cyan] Would analyze releases for {package_name}",
|
|
370
|
+
f"[cyan]📦[/ cyan] Would analyze releases for {package_name}",
|
|
373
371
|
)
|
|
374
372
|
self.console.print(
|
|
375
|
-
f"[cyan]🔧[/cyan] Would keep {keep_releases} most recent releases",
|
|
373
|
+
f"[cyan]🔧[/ cyan] Would keep {keep_releases} most recent releases",
|
|
376
374
|
)
|
|
377
375
|
|
|
378
376
|
return True
|
|
379
377
|
except Exception as e:
|
|
380
|
-
self.console.print(f"[red]❌[/red] Cleanup error: {e}")
|
|
378
|
+
self.console.print(f"[red]❌[/ red] Cleanup error: {e}")
|
|
381
379
|
return False
|
|
382
380
|
|
|
383
381
|
def create_git_tag(self, version: str) -> bool:
|
|
384
382
|
try:
|
|
385
383
|
if self.dry_run:
|
|
386
384
|
self.console.print(
|
|
387
|
-
f"[yellow]🔍[/yellow] Would create git tag: v{version}",
|
|
385
|
+
f"[yellow]🔍[/ yellow] Would create git tag: v{version}",
|
|
388
386
|
)
|
|
389
387
|
return True
|
|
390
388
|
result = self._run_command(["git", "tag", f"v{version}"])
|
|
391
389
|
if result.returncode == 0:
|
|
392
|
-
self.console.print(f"[green]🏷️[/green] Created git tag: v{version}")
|
|
390
|
+
self.console.print(f"[green]🏷️[/ green] Created git tag: v{version}")
|
|
393
391
|
push_result = self._run_command(
|
|
394
392
|
["git", "push", "origin", f"v{version}"],
|
|
395
393
|
)
|
|
396
394
|
if push_result.returncode == 0:
|
|
397
|
-
self.console.print("[green]📤[/green] Pushed tag to remote")
|
|
395
|
+
self.console.print("[green]📤[/ green] Pushed tag to remote")
|
|
398
396
|
else:
|
|
399
397
|
self.console.print(
|
|
400
|
-
f"[yellow]⚠️[/yellow] Tag created but push failed: {push_result.stderr}",
|
|
398
|
+
f"[yellow]⚠️[/ yellow] Tag created but push failed: {push_result.stderr}",
|
|
401
399
|
)
|
|
402
400
|
|
|
403
401
|
return True
|
|
404
402
|
self.console.print(
|
|
405
|
-
f"[red]❌[/red] Failed to create tag: {result.stderr}",
|
|
403
|
+
f"[red]❌[/ red] Failed to create tag: {result.stderr}",
|
|
406
404
|
)
|
|
407
405
|
return False
|
|
408
406
|
except Exception as e:
|
|
409
|
-
self.console.print(f"[red]❌[/red] Tag creation error: {e}")
|
|
407
|
+
self.console.print(f"[red]❌[/ red] Tag creation error: {e}")
|
|
410
408
|
return False
|
|
411
409
|
|
|
412
410
|
def get_package_info(self) -> dict[str, t.Any]:
|
|
@@ -429,5 +427,5 @@ class PublishManagerImpl:
|
|
|
429
427
|
"python_requires": project.get("requires-python", ""),
|
|
430
428
|
}
|
|
431
429
|
except Exception as e:
|
|
432
|
-
self.console.print(f"[yellow]⚠️[/yellow] Error reading package info: {e}")
|
|
430
|
+
self.console.print(f"[yellow]⚠️[/ yellow] Error reading package info: {e}")
|
|
433
431
|
return {}
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
"""Test command building and configuration.
|
|
2
|
-
|
|
3
|
-
This module handles pytest command construction with various options and configurations.
|
|
4
|
-
Split from test_manager.py for better separation of concerns.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
1
|
from pathlib import Path
|
|
8
2
|
|
|
9
3
|
from crackerjack.models.protocols import OptionsProtocol
|
|
10
4
|
|
|
11
5
|
|
|
12
6
|
class TestCommandBuilder:
|
|
13
|
-
"""Builds pytest commands with appropriate options and configurations."""
|
|
14
|
-
|
|
15
7
|
def __init__(self, pkg_path: Path) -> None:
|
|
16
8
|
self.pkg_path = pkg_path
|
|
17
9
|
|
|
18
10
|
def build_command(self, options: OptionsProtocol) -> list[str]:
|
|
19
|
-
"""Build complete pytest command with all options."""
|
|
20
11
|
cmd = ["python", "-m", "pytest"]
|
|
21
12
|
|
|
22
13
|
self._add_coverage_options(cmd, options)
|
|
@@ -29,16 +20,13 @@ class TestCommandBuilder:
|
|
|
29
20
|
return cmd
|
|
30
21
|
|
|
31
22
|
def get_optimal_workers(self, options: OptionsProtocol) -> int:
|
|
32
|
-
"""Calculate optimal number of pytest workers based on system and configuration."""
|
|
33
23
|
if hasattr(options, "test_workers") and options.test_workers:
|
|
34
24
|
return options.test_workers
|
|
35
25
|
|
|
36
|
-
# Auto-detect based on CPU count
|
|
37
26
|
import multiprocessing
|
|
38
27
|
|
|
39
28
|
cpu_count = multiprocessing.cpu_count()
|
|
40
29
|
|
|
41
|
-
# Conservative worker count to avoid overwhelming the system
|
|
42
30
|
if cpu_count <= 2:
|
|
43
31
|
return 1
|
|
44
32
|
elif cpu_count <= 4:
|
|
@@ -48,35 +36,29 @@ class TestCommandBuilder:
|
|
|
48
36
|
return 4
|
|
49
37
|
|
|
50
38
|
def get_test_timeout(self, options: OptionsProtocol) -> int:
|
|
51
|
-
"""Get test timeout based on options or default."""
|
|
52
39
|
if hasattr(options, "test_timeout") and options.test_timeout:
|
|
53
40
|
return options.test_timeout
|
|
54
41
|
|
|
55
|
-
# Default timeout based on test configuration
|
|
56
42
|
if hasattr(options, "benchmark") and options.benchmark:
|
|
57
|
-
return 900
|
|
58
|
-
return 300
|
|
43
|
+
return 900
|
|
44
|
+
return 300
|
|
59
45
|
|
|
60
46
|
def _add_coverage_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
61
|
-
"""Add coverage-related options to command."""
|
|
62
|
-
# Always include coverage for comprehensive testing
|
|
63
47
|
cmd.extend(
|
|
64
48
|
[
|
|
65
49
|
"--cov=crackerjack",
|
|
66
50
|
"--cov-report=term-missing",
|
|
67
51
|
"--cov-report=html",
|
|
68
|
-
"--cov-fail-under=0",
|
|
52
|
+
"--cov-fail-under=0",
|
|
69
53
|
]
|
|
70
54
|
)
|
|
71
55
|
|
|
72
56
|
def _add_worker_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
73
|
-
"""Add parallel execution options to command."""
|
|
74
57
|
workers = self.get_optimal_workers(options)
|
|
75
58
|
if workers > 1:
|
|
76
59
|
cmd.extend(["-n", str(workers)])
|
|
77
60
|
|
|
78
61
|
def _add_benchmark_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
79
|
-
"""Add benchmark-specific options to command."""
|
|
80
62
|
if hasattr(options, "benchmark") and options.benchmark:
|
|
81
63
|
cmd.extend(
|
|
82
64
|
[
|
|
@@ -87,27 +69,21 @@ class TestCommandBuilder:
|
|
|
87
69
|
)
|
|
88
70
|
|
|
89
71
|
def _add_timeout_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
90
|
-
"""Add timeout options to command."""
|
|
91
72
|
timeout = self.get_test_timeout(options)
|
|
92
73
|
cmd.extend(["--timeout", str(timeout)])
|
|
93
74
|
|
|
94
75
|
def _add_verbosity_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
95
|
-
"""Add verbosity and output formatting options."""
|
|
96
|
-
# Always use verbose output for better progress tracking
|
|
97
76
|
cmd.append("-v")
|
|
98
77
|
|
|
99
|
-
# Add useful output options
|
|
100
78
|
cmd.extend(
|
|
101
79
|
[
|
|
102
|
-
"--tb=short",
|
|
103
|
-
"--strict-markers",
|
|
104
|
-
"--strict-config",
|
|
80
|
+
"--tb=short",
|
|
81
|
+
"--strict-markers",
|
|
82
|
+
"--strict-config",
|
|
105
83
|
]
|
|
106
84
|
)
|
|
107
85
|
|
|
108
86
|
def _add_test_path(self, cmd: list[str]) -> None:
|
|
109
|
-
"""Add test path to command."""
|
|
110
|
-
# Add tests directory if it exists, otherwise current directory
|
|
111
87
|
test_paths = ["tests", "test"]
|
|
112
88
|
|
|
113
89
|
for test_path in test_paths:
|
|
@@ -116,14 +92,11 @@ class TestCommandBuilder:
|
|
|
116
92
|
cmd.append(str(full_path))
|
|
117
93
|
return
|
|
118
94
|
|
|
119
|
-
# Fallback to current directory
|
|
120
95
|
cmd.append(str(self.pkg_path))
|
|
121
96
|
|
|
122
97
|
def build_specific_test_command(self, test_pattern: str) -> list[str]:
|
|
123
|
-
"""Build command for running specific tests matching a pattern."""
|
|
124
98
|
cmd = ["python", "-m", "pytest", "-v"]
|
|
125
99
|
|
|
126
|
-
# Add basic coverage
|
|
127
100
|
cmd.extend(
|
|
128
101
|
[
|
|
129
102
|
"--cov=crackerjack",
|
|
@@ -131,16 +104,13 @@ class TestCommandBuilder:
|
|
|
131
104
|
]
|
|
132
105
|
)
|
|
133
106
|
|
|
134
|
-
# Add the test pattern
|
|
135
107
|
cmd.extend(["-k", test_pattern])
|
|
136
108
|
|
|
137
|
-
# Add test path
|
|
138
109
|
self._add_test_path(cmd)
|
|
139
110
|
|
|
140
111
|
return cmd
|
|
141
112
|
|
|
142
113
|
def build_validation_command(self) -> list[str]:
|
|
143
|
-
"""Build command for test environment validation."""
|
|
144
114
|
return [
|
|
145
115
|
"python",
|
|
146
116
|
"-m",
|