crackerjack 0.27.0__py3-none-any.whl → 0.27.1__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/.pre-commit-config.yaml +0 -12
- crackerjack/__main__.py +3 -2
- crackerjack/crackerjack.py +241 -85
- crackerjack/interactive.py +1 -1
- crackerjack/py313.py +2 -2
- crackerjack/pyproject.toml +1 -19
- {crackerjack-0.27.0.dist-info → crackerjack-0.27.1.dist-info}/METADATA +1 -1
- crackerjack-0.27.1.dist-info/RECORD +16 -0
- crackerjack-0.27.0.dist-info/RECORD +0 -16
- {crackerjack-0.27.0.dist-info → crackerjack-0.27.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.27.0.dist-info → crackerjack-0.27.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -91,10 +91,6 @@ repos:
|
|
|
91
91
|
- --aggressive
|
|
92
92
|
- --only-without-imports
|
|
93
93
|
- --guess-common-names
|
|
94
|
-
- --cache-dir=.autotyping-cache
|
|
95
|
-
- --workers=4
|
|
96
|
-
- --max-line-length=88
|
|
97
|
-
- --exclude-name=test_*,conftest
|
|
98
94
|
- crackerjack
|
|
99
95
|
types_or: [ python, pyi ]
|
|
100
96
|
language: python
|
|
@@ -116,11 +112,3 @@ repos:
|
|
|
116
112
|
hooks:
|
|
117
113
|
- id: pyright
|
|
118
114
|
stages: [pre-push, manual]
|
|
119
|
-
|
|
120
|
-
# Additional quality and security checks
|
|
121
|
-
- repo: https://github.com/PyCQA/pydocstyle
|
|
122
|
-
rev: 6.3.0
|
|
123
|
-
hooks:
|
|
124
|
-
- id: pydocstyle
|
|
125
|
-
args: ["--config=pyproject.toml"]
|
|
126
|
-
stages: [pre-push, manual]
|
crackerjack/__main__.py
CHANGED
|
@@ -4,7 +4,8 @@ from enum import Enum
|
|
|
4
4
|
import typer
|
|
5
5
|
from pydantic import BaseModel, field_validator
|
|
6
6
|
from rich.console import Console
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
from .crackerjack import create_crackerjack_runner
|
|
8
9
|
|
|
9
10
|
console = Console(force_terminal=True)
|
|
10
11
|
app = typer.Typer(
|
|
@@ -206,7 +207,7 @@ def main(
|
|
|
206
207
|
|
|
207
208
|
os.environ["AI_AGENT"] = "1"
|
|
208
209
|
if interactive:
|
|
209
|
-
from
|
|
210
|
+
from .interactive import launch_interactive_cli
|
|
210
211
|
|
|
211
212
|
try:
|
|
212
213
|
from importlib.metadata import version
|
crackerjack/crackerjack.py
CHANGED
|
@@ -18,7 +18,8 @@ import aiofiles
|
|
|
18
18
|
from pydantic import BaseModel
|
|
19
19
|
from rich.console import Console
|
|
20
20
|
from tomli_w import dumps
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
from .errors import ErrorCode, ExecutionError
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
@@ -361,7 +362,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
361
362
|
|
|
362
363
|
def remove_docstrings(self, code: str) -> str:
|
|
363
364
|
lines = code.split("\n")
|
|
364
|
-
cleaned_lines = []
|
|
365
|
+
cleaned_lines: list[str] = []
|
|
365
366
|
docstring_state = self._initialize_docstring_state()
|
|
366
367
|
for i, line in enumerate(lines):
|
|
367
368
|
handled, result_line = self._process_line(lines, i, line, docstring_state)
|
|
@@ -418,7 +419,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
418
419
|
|
|
419
420
|
def remove_line_comments(self, code: str) -> str:
|
|
420
421
|
lines = code.split("\n")
|
|
421
|
-
cleaned_lines = []
|
|
422
|
+
cleaned_lines: list[str] = []
|
|
422
423
|
for line in lines:
|
|
423
424
|
if not line.strip():
|
|
424
425
|
cleaned_lines.append(line)
|
|
@@ -429,7 +430,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
429
430
|
return "\n".join(cleaned_lines)
|
|
430
431
|
|
|
431
432
|
def _process_line_for_comments(self, line: str) -> str:
|
|
432
|
-
result = []
|
|
433
|
+
result: list[str] = []
|
|
433
434
|
string_state = {"in_string": None}
|
|
434
435
|
for i, char in enumerate(line):
|
|
435
436
|
if self._handle_string_character(char, i, line, string_state, result):
|
|
@@ -483,7 +484,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
483
484
|
|
|
484
485
|
def remove_extra_whitespace(self, code: str) -> str:
|
|
485
486
|
lines = code.split("\n")
|
|
486
|
-
cleaned_lines = []
|
|
487
|
+
cleaned_lines: list[str] = []
|
|
487
488
|
function_tracker = {"in_function": False, "function_indent": 0}
|
|
488
489
|
import_tracker = {"in_imports": False, "last_import_type": None}
|
|
489
490
|
for i, line in enumerate(lines):
|
|
@@ -854,8 +855,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
854
855
|
from crackerjack.errors import ExecutionError, handle_error
|
|
855
856
|
|
|
856
857
|
try:
|
|
857
|
-
async with aiofiles.open(file_path, encoding="utf-8") as f:
|
|
858
|
-
code = await f.read()
|
|
858
|
+
async with aiofiles.open(file_path, encoding="utf-8") as f: # type: ignore[misc]
|
|
859
|
+
code = await f.read() # type: ignore[misc]
|
|
859
860
|
original_code = code
|
|
860
861
|
cleaning_failed = False
|
|
861
862
|
try:
|
|
@@ -890,8 +891,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
890
891
|
)
|
|
891
892
|
code = original_code
|
|
892
893
|
cleaning_failed = True
|
|
893
|
-
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
894
|
-
await f.write(code)
|
|
894
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f: # type: ignore[misc]
|
|
895
|
+
await f.write(code) # type: ignore[misc]
|
|
895
896
|
if cleaning_failed:
|
|
896
897
|
self.console.print(
|
|
897
898
|
f"[bold yellow]⚡ Partially cleaned:[/bold yellow] [dim bright_white]{file_path}[/dim bright_white]"
|
|
@@ -965,8 +966,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
965
966
|
suffix=".py", mode="w+", delete=False
|
|
966
967
|
) as temp:
|
|
967
968
|
temp_path = Path(temp.name)
|
|
968
|
-
async with aiofiles.open(temp_path, "w", encoding="utf-8") as f:
|
|
969
|
-
await f.write(code)
|
|
969
|
+
async with aiofiles.open(temp_path, "w", encoding="utf-8") as f: # type: ignore[misc]
|
|
970
|
+
await f.write(code) # type: ignore[misc]
|
|
970
971
|
try:
|
|
971
972
|
proc = await asyncio.create_subprocess_exec(
|
|
972
973
|
"uv",
|
|
@@ -977,10 +978,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
977
978
|
stdout=asyncio.subprocess.PIPE,
|
|
978
979
|
stderr=asyncio.subprocess.PIPE,
|
|
979
980
|
)
|
|
980
|
-
|
|
981
|
+
_, stderr = await proc.communicate()
|
|
981
982
|
if proc.returncode == 0:
|
|
982
|
-
async with aiofiles.open(temp_path, encoding="utf-8") as f:
|
|
983
|
-
formatted_code = await f.read()
|
|
983
|
+
async with aiofiles.open(temp_path, encoding="utf-8") as f: # type: ignore[misc]
|
|
984
|
+
formatted_code = await f.read() # type: ignore[misc]
|
|
984
985
|
else:
|
|
985
986
|
self.console.print(
|
|
986
987
|
f"[bold bright_yellow]⚠️ Warning: Ruff format failed with return code {proc.returncode}[/bold bright_yellow]"
|
|
@@ -1025,7 +1026,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
1025
1026
|
return code
|
|
1026
1027
|
|
|
1027
1028
|
async def _cleanup_cache_directories_async(self, pkg_dir: Path) -> None:
|
|
1028
|
-
def cleanup_sync():
|
|
1029
|
+
def cleanup_sync() -> None:
|
|
1029
1030
|
with suppress(PermissionError, OSError):
|
|
1030
1031
|
pycache_dir = pkg_dir / "__pycache__"
|
|
1031
1032
|
if pycache_dir.exists():
|
|
@@ -1171,7 +1172,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1171
1172
|
) -> None:
|
|
1172
1173
|
python_version_pattern = "\\s*W*(\\d\\.\\d*)"
|
|
1173
1174
|
requires_python = our_toml_config.get("project", {}).get("requires-python", "")
|
|
1174
|
-
classifiers = []
|
|
1175
|
+
classifiers: list[str] = []
|
|
1175
1176
|
for classifier in pkg_toml_config.get("project", {}).get("classifiers", []):
|
|
1176
1177
|
classifier = re.sub(
|
|
1177
1178
|
python_version_pattern, f" {self.python_version}", classifier
|
|
@@ -1186,7 +1187,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1186
1187
|
self.pkg_toml_path.write_text(dumps(pkg_toml_config))
|
|
1187
1188
|
|
|
1188
1189
|
def copy_configs(self) -> None:
|
|
1189
|
-
configs_to_add = []
|
|
1190
|
+
configs_to_add: list[str] = []
|
|
1190
1191
|
for config in config_files:
|
|
1191
1192
|
config_path = self.our_path / config
|
|
1192
1193
|
pkg_config_path = self.pkg_path / config
|
|
@@ -1238,7 +1239,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1238
1239
|
len(py_files) + len(js_files) + len(yaml_files) + len(md_files)
|
|
1239
1240
|
)
|
|
1240
1241
|
total_size = 0
|
|
1241
|
-
for files in
|
|
1242
|
+
for files in (py_files, js_files, yaml_files, md_files):
|
|
1242
1243
|
for file_path in files:
|
|
1243
1244
|
try:
|
|
1244
1245
|
total_size += file_path.stat().st_size
|
|
@@ -1395,7 +1396,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1395
1396
|
return hook_results
|
|
1396
1397
|
|
|
1397
1398
|
def _parse_hook_output(self, stdout: str, stderr: str) -> list[HookResult]:
|
|
1398
|
-
hook_results = []
|
|
1399
|
+
hook_results: list[HookResult] = []
|
|
1399
1400
|
lines = stdout.split("\n")
|
|
1400
1401
|
for line in lines:
|
|
1401
1402
|
if "..." in line and (
|
|
@@ -1482,7 +1483,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1482
1483
|
def _generate_optimization_suggestions(
|
|
1483
1484
|
self, hook_results: list[HookResult]
|
|
1484
1485
|
) -> list[str]:
|
|
1485
|
-
suggestions = []
|
|
1486
|
+
suggestions: list[str] = []
|
|
1486
1487
|
|
|
1487
1488
|
for hook in hook_results:
|
|
1488
1489
|
if hook.duration > 5.0:
|
|
@@ -1578,35 +1579,40 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1578
1579
|
def _parse_refurb_results(self) -> dict[str, t.Any]:
|
|
1579
1580
|
return {"suggestions": 0, "patterns_modernized": []}
|
|
1580
1581
|
|
|
1582
|
+
def _parse_complexity_list(
|
|
1583
|
+
self, complexity_data: list[dict[str, t.Any]]
|
|
1584
|
+
) -> dict[str, t.Any]:
|
|
1585
|
+
if not complexity_data:
|
|
1586
|
+
return {
|
|
1587
|
+
"average_complexity": 0,
|
|
1588
|
+
"max_complexity": 0,
|
|
1589
|
+
"total_functions": 0,
|
|
1590
|
+
}
|
|
1591
|
+
complexities = [item.get("complexity", 0) for item in complexity_data]
|
|
1592
|
+
return {
|
|
1593
|
+
"average_complexity": sum(complexities) / len(complexities)
|
|
1594
|
+
if complexities
|
|
1595
|
+
else 0,
|
|
1596
|
+
"max_complexity": max(complexities) if complexities else 0,
|
|
1597
|
+
"total_functions": len(complexities),
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
def _parse_complexity_dict(
|
|
1601
|
+
self, complexity_data: dict[str, t.Any]
|
|
1602
|
+
) -> dict[str, t.Any]:
|
|
1603
|
+
return {
|
|
1604
|
+
"average_complexity": complexity_data.get("average", 0),
|
|
1605
|
+
"max_complexity": complexity_data.get("max", 0),
|
|
1606
|
+
"total_functions": complexity_data.get("total", 0),
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1581
1609
|
def _parse_complexity_results(self) -> dict[str, t.Any]:
|
|
1582
1610
|
try:
|
|
1583
1611
|
with open("complexipy.json", encoding="utf-8") as f:
|
|
1584
1612
|
complexity_data = json.load(f)
|
|
1585
1613
|
if isinstance(complexity_data, list):
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
"average_complexity": 0,
|
|
1589
|
-
"max_complexity": 0,
|
|
1590
|
-
"total_functions": 0,
|
|
1591
|
-
}
|
|
1592
|
-
complexities = [
|
|
1593
|
-
item.get("complexity", 0)
|
|
1594
|
-
for item in complexity_data
|
|
1595
|
-
if isinstance(item, dict)
|
|
1596
|
-
]
|
|
1597
|
-
return {
|
|
1598
|
-
"average_complexity": sum(complexities) / len(complexities)
|
|
1599
|
-
if complexities
|
|
1600
|
-
else 0,
|
|
1601
|
-
"max_complexity": max(complexities) if complexities else 0,
|
|
1602
|
-
"total_functions": len(complexities),
|
|
1603
|
-
}
|
|
1604
|
-
else:
|
|
1605
|
-
return {
|
|
1606
|
-
"average_complexity": complexity_data.get("average", 0),
|
|
1607
|
-
"max_complexity": complexity_data.get("max", 0),
|
|
1608
|
-
"total_functions": complexity_data.get("total", 0),
|
|
1609
|
-
}
|
|
1614
|
+
return self._parse_complexity_list(complexity_data)
|
|
1615
|
+
return self._parse_complexity_dict(complexity_data)
|
|
1610
1616
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
1611
1617
|
return {"status": "complexity_analysis_not_available"}
|
|
1612
1618
|
|
|
@@ -1653,7 +1659,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1653
1659
|
return 95.0
|
|
1654
1660
|
|
|
1655
1661
|
def _generate_quality_recommendations(self) -> list[str]:
|
|
1656
|
-
recommendations = []
|
|
1662
|
+
recommendations: list[str] = []
|
|
1657
1663
|
recommendations.extend(
|
|
1658
1664
|
[
|
|
1659
1665
|
"Consider adding more integration tests",
|
|
@@ -1749,18 +1755,16 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1749
1755
|
return "unknown"
|
|
1750
1756
|
|
|
1751
1757
|
def _analyze_directory_structure(self) -> dict[str, t.Any]:
|
|
1752
|
-
directories = [
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
)
|
|
1758
|
+
directories = [
|
|
1759
|
+
{
|
|
1760
|
+
"name": item.name,
|
|
1761
|
+
"type": self._classify_directory(item),
|
|
1762
|
+
"file_count": len(list(item.rglob("*"))),
|
|
1763
|
+
}
|
|
1764
|
+
for item in self.pkg_path.iterdir()
|
|
1765
|
+
if item.is_dir()
|
|
1766
|
+
and not item.name.startswith((".git", "__pycache__", ".venv"))
|
|
1767
|
+
]
|
|
1764
1768
|
return {"directories": directories, "total_directories": len(directories)}
|
|
1765
1769
|
|
|
1766
1770
|
def _analyze_file_distribution(self) -> dict[str, t.Any]:
|
|
@@ -1778,17 +1782,15 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1778
1782
|
|
|
1779
1783
|
def _analyze_dependencies(self) -> dict[str, t.Any]:
|
|
1780
1784
|
deps = {"status": "analysis_not_implemented"}
|
|
1781
|
-
|
|
1785
|
+
with suppress(Exception):
|
|
1782
1786
|
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
1783
1787
|
if pyproject_path.exists():
|
|
1784
1788
|
pyproject_path.read_text(encoding="utf-8")
|
|
1785
1789
|
deps = {"source": "pyproject.toml", "status": "detected"}
|
|
1786
|
-
except Exception:
|
|
1787
|
-
pass
|
|
1788
1790
|
return deps
|
|
1789
1791
|
|
|
1790
1792
|
def _analyze_configuration_files(self) -> list[str]:
|
|
1791
|
-
config_files = []
|
|
1793
|
+
config_files: list[str] = []
|
|
1792
1794
|
config_patterns = ["*.toml", "*.yaml", "*.yml", "*.ini", "*.cfg", ".env*"]
|
|
1793
1795
|
for pattern in config_patterns:
|
|
1794
1796
|
config_files.extend([f.name for f in self.pkg_path.glob(pattern)])
|
|
@@ -1859,8 +1861,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1859
1861
|
return "hidden"
|
|
1860
1862
|
elif (directory / "__init__.py").exists():
|
|
1861
1863
|
return "python_package"
|
|
1862
|
-
|
|
1863
|
-
return "general"
|
|
1864
|
+
return "general"
|
|
1864
1865
|
|
|
1865
1866
|
def _collect_environment_info(self) -> dict[str, t.Any]:
|
|
1866
1867
|
return {
|
|
@@ -1871,7 +1872,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1871
1872
|
}
|
|
1872
1873
|
|
|
1873
1874
|
def _identify_common_issues(self) -> list[str]:
|
|
1874
|
-
issues = []
|
|
1875
|
+
issues: list[str] = []
|
|
1875
1876
|
if not (self.pkg_path / "pyproject.toml").exists():
|
|
1876
1877
|
issues.append("Missing pyproject.toml configuration")
|
|
1877
1878
|
if not (self.pkg_path / ".gitignore").exists():
|
|
@@ -2048,6 +2049,58 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2048
2049
|
|
|
2049
2050
|
return hook_results
|
|
2050
2051
|
|
|
2052
|
+
def _get_analysis_files(self) -> list[str]:
|
|
2053
|
+
analysis_files: list[str] = []
|
|
2054
|
+
if (
|
|
2055
|
+
hasattr(self, "options")
|
|
2056
|
+
and self.options
|
|
2057
|
+
and getattr(self.options, "ai_agent", False)
|
|
2058
|
+
):
|
|
2059
|
+
analysis_files.extend(
|
|
2060
|
+
[
|
|
2061
|
+
"test-results.xml",
|
|
2062
|
+
"coverage.json",
|
|
2063
|
+
"benchmark.json",
|
|
2064
|
+
"ai-agent-summary.json",
|
|
2065
|
+
]
|
|
2066
|
+
)
|
|
2067
|
+
return analysis_files
|
|
2068
|
+
|
|
2069
|
+
def _generate_analysis_files(self, hook_results: list[HookResult]) -> None:
|
|
2070
|
+
if not (
|
|
2071
|
+
hasattr(self, "options")
|
|
2072
|
+
and self.options
|
|
2073
|
+
and getattr(self.options, "ai_agent", False)
|
|
2074
|
+
):
|
|
2075
|
+
return
|
|
2076
|
+
try:
|
|
2077
|
+
import json
|
|
2078
|
+
|
|
2079
|
+
summary = {
|
|
2080
|
+
"status": "success"
|
|
2081
|
+
if all(hr.status == "Passed" for hr in hook_results)
|
|
2082
|
+
else "failed",
|
|
2083
|
+
"hook_results": [
|
|
2084
|
+
{
|
|
2085
|
+
"name": hr.name,
|
|
2086
|
+
"status": hr.status,
|
|
2087
|
+
"duration": hr.duration,
|
|
2088
|
+
"issues": hr.issues_found
|
|
2089
|
+
if hasattr(hr, "issues_found")
|
|
2090
|
+
else [],
|
|
2091
|
+
}
|
|
2092
|
+
for hr in hook_results
|
|
2093
|
+
],
|
|
2094
|
+
"total_duration": sum(hr.duration for hr in hook_results),
|
|
2095
|
+
"files_analyzed": len(hook_results),
|
|
2096
|
+
}
|
|
2097
|
+
with open("ai-agent-summary.json", "w") as f:
|
|
2098
|
+
json.dump(summary, f, indent=2)
|
|
2099
|
+
except Exception as e:
|
|
2100
|
+
self.console.print(
|
|
2101
|
+
f"[yellow]Warning: Failed to generate AI summary: {e}[/yellow]"
|
|
2102
|
+
)
|
|
2103
|
+
|
|
2051
2104
|
|
|
2052
2105
|
class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
2053
2106
|
our_path: Path = Path(__file__).parent
|
|
@@ -2096,6 +2149,8 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2096
2149
|
"[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]"
|
|
2097
2150
|
)
|
|
2098
2151
|
self.console.print("-" * 80 + "\n")
|
|
2152
|
+
assert self.config_manager is not None
|
|
2153
|
+
assert self.project_manager is not None
|
|
2099
2154
|
self.config_manager.pkg_name = self.pkg_name
|
|
2100
2155
|
self.project_manager.pkg_name = self.pkg_name
|
|
2101
2156
|
self.project_manager.pkg_dir = self.pkg_dir
|
|
@@ -2309,31 +2364,38 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2309
2364
|
except (OSError, PermissionError):
|
|
2310
2365
|
return "medium"
|
|
2311
2366
|
|
|
2367
|
+
def _calculate_test_metrics(self, test_files: list[Path]) -> tuple[int, int]:
|
|
2368
|
+
total_test_size = 0
|
|
2369
|
+
slow_tests = 0
|
|
2370
|
+
for test_file in test_files:
|
|
2371
|
+
try:
|
|
2372
|
+
size = test_file.stat().st_size
|
|
2373
|
+
total_test_size += size
|
|
2374
|
+
if size > 30_000 or "integration" in test_file.name.lower():
|
|
2375
|
+
slow_tests += 1
|
|
2376
|
+
except (OSError, PermissionError):
|
|
2377
|
+
continue
|
|
2378
|
+
return total_test_size, slow_tests
|
|
2379
|
+
|
|
2380
|
+
def _determine_test_complexity(
|
|
2381
|
+
self, test_count: int, avg_size: float, slow_ratio: float
|
|
2382
|
+
) -> str:
|
|
2383
|
+
if test_count > 100 or avg_size > 25_000 or slow_ratio > 0.4:
|
|
2384
|
+
return "high"
|
|
2385
|
+
elif test_count > 50 or avg_size > 15_000 or slow_ratio > 0.2:
|
|
2386
|
+
return "medium"
|
|
2387
|
+
return "low"
|
|
2388
|
+
|
|
2312
2389
|
def _analyze_test_workload(self) -> dict[str, t.Any]:
|
|
2313
2390
|
try:
|
|
2314
2391
|
test_files = self._get_cached_files_with_mtime("test_*.py")
|
|
2315
2392
|
py_files = self._get_cached_files_with_mtime("*.py")
|
|
2316
|
-
total_test_size =
|
|
2317
|
-
slow_tests = 0
|
|
2318
|
-
for test_file in test_files:
|
|
2319
|
-
try:
|
|
2320
|
-
size = test_file.stat().st_size
|
|
2321
|
-
total_test_size += size
|
|
2322
|
-
if size > 30_000 or "integration" in test_file.name.lower():
|
|
2323
|
-
slow_tests += 1
|
|
2324
|
-
except (OSError, PermissionError):
|
|
2325
|
-
continue
|
|
2393
|
+
total_test_size, slow_tests = self._calculate_test_metrics(test_files)
|
|
2326
2394
|
avg_test_size = total_test_size / len(test_files) if test_files else 0
|
|
2327
2395
|
slow_test_ratio = slow_tests / len(test_files) if test_files else 0
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
len(test_files) > 50 or avg_test_size > 15_000 or slow_test_ratio > 0.2
|
|
2332
|
-
):
|
|
2333
|
-
complexity = "medium"
|
|
2334
|
-
else:
|
|
2335
|
-
complexity = "low"
|
|
2336
|
-
|
|
2396
|
+
complexity = self._determine_test_complexity(
|
|
2397
|
+
len(test_files), avg_test_size, slow_test_ratio
|
|
2398
|
+
)
|
|
2337
2399
|
return {
|
|
2338
2400
|
"total_files": len(py_files),
|
|
2339
2401
|
"test_files": len(test_files),
|
|
@@ -2354,8 +2416,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2354
2416
|
return min(cpu_count // 3, 2)
|
|
2355
2417
|
elif workload["complexity"] == "medium":
|
|
2356
2418
|
return min(cpu_count // 2, 4)
|
|
2357
|
-
|
|
2358
|
-
return min(cpu_count, 8)
|
|
2419
|
+
return min(cpu_count, 8)
|
|
2359
2420
|
|
|
2360
2421
|
def _print_ai_agent_files(self, options: t.Any) -> None:
|
|
2361
2422
|
if getattr(options, "ai_agent", False):
|
|
@@ -2507,6 +2568,99 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2507
2568
|
stderr.decode() if stderr else "",
|
|
2508
2569
|
)
|
|
2509
2570
|
|
|
2571
|
+
def _run_comprehensive_quality_checks(self, options: OptionsProtocol) -> None:
|
|
2572
|
+
if options.skip_hooks or (
|
|
2573
|
+
options.test
|
|
2574
|
+
and not any([options.publish, options.bump, options.commit, options.all])
|
|
2575
|
+
):
|
|
2576
|
+
return
|
|
2577
|
+
needs_comprehensive = any(
|
|
2578
|
+
[options.publish, options.bump, options.commit, options.all]
|
|
2579
|
+
)
|
|
2580
|
+
if not needs_comprehensive:
|
|
2581
|
+
return
|
|
2582
|
+
self.console.print("\n" + "-" * 80)
|
|
2583
|
+
self.console.print(
|
|
2584
|
+
"[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
|
|
2585
|
+
)
|
|
2586
|
+
self.console.print("-" * 80 + "\n")
|
|
2587
|
+
cmd = [
|
|
2588
|
+
"uv",
|
|
2589
|
+
"run",
|
|
2590
|
+
"pre-commit",
|
|
2591
|
+
"run",
|
|
2592
|
+
"--all-files",
|
|
2593
|
+
"--hook-stage=manual",
|
|
2594
|
+
"-c",
|
|
2595
|
+
".pre-commit-config.yaml",
|
|
2596
|
+
]
|
|
2597
|
+
result = self.execute_command(cmd, capture_output=True, text=True)
|
|
2598
|
+
if result.returncode > 0:
|
|
2599
|
+
self.console.print(
|
|
2600
|
+
"\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
|
|
2601
|
+
)
|
|
2602
|
+
self.console.print(f"[dim]STDOUT:[/dim]\n{result.stdout}")
|
|
2603
|
+
if result.stderr:
|
|
2604
|
+
self.console.print(f"[dim]STDERR:[/dim]\n{result.stderr}")
|
|
2605
|
+
self.console.print(
|
|
2606
|
+
"\n[bold red]Cannot proceed with publishing/committing until all quality checks pass. [/bold red]"
|
|
2607
|
+
)
|
|
2608
|
+
raise SystemExit(1)
|
|
2609
|
+
else:
|
|
2610
|
+
self.console.print(
|
|
2611
|
+
"[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
async def _run_comprehensive_quality_checks_async(
|
|
2615
|
+
self, options: OptionsProtocol
|
|
2616
|
+
) -> None:
|
|
2617
|
+
if options.skip_hooks or (
|
|
2618
|
+
options.test
|
|
2619
|
+
and not any([options.publish, options.bump, options.commit, options.all])
|
|
2620
|
+
):
|
|
2621
|
+
return
|
|
2622
|
+
|
|
2623
|
+
needs_comprehensive = any(
|
|
2624
|
+
[options.publish, options.bump, options.commit, options.all]
|
|
2625
|
+
)
|
|
2626
|
+
|
|
2627
|
+
if not needs_comprehensive:
|
|
2628
|
+
return
|
|
2629
|
+
|
|
2630
|
+
self.console.print("\n" + "-" * 80)
|
|
2631
|
+
self.console.print(
|
|
2632
|
+
"[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
|
|
2633
|
+
)
|
|
2634
|
+
self.console.print("-" * 80 + "\n")
|
|
2635
|
+
|
|
2636
|
+
cmd = [
|
|
2637
|
+
"uv",
|
|
2638
|
+
"run",
|
|
2639
|
+
"pre-commit",
|
|
2640
|
+
"run",
|
|
2641
|
+
"--all-files",
|
|
2642
|
+
"--hook-stage=manual",
|
|
2643
|
+
"-c",
|
|
2644
|
+
".pre-commit-config.yaml",
|
|
2645
|
+
]
|
|
2646
|
+
|
|
2647
|
+
result = await self.execute_command_async(cmd)
|
|
2648
|
+
|
|
2649
|
+
if result.returncode > 0:
|
|
2650
|
+
self.console.print(
|
|
2651
|
+
"\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
|
|
2652
|
+
)
|
|
2653
|
+
if result.stderr:
|
|
2654
|
+
self.console.print(f"[dim]Error details: {result.stderr}[/dim]")
|
|
2655
|
+
self.console.print(
|
|
2656
|
+
"\n[bold red]Cannot proceed with publishing/committing until all quality checks pass. [/bold red]"
|
|
2657
|
+
)
|
|
2658
|
+
raise SystemExit(1)
|
|
2659
|
+
else:
|
|
2660
|
+
self.console.print(
|
|
2661
|
+
"[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
|
|
2662
|
+
)
|
|
2663
|
+
|
|
2510
2664
|
def process(self, options: OptionsProtocol) -> None:
|
|
2511
2665
|
self.console.print("\n" + "-" * 80)
|
|
2512
2666
|
self.console.print(
|
|
@@ -2533,6 +2687,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2533
2687
|
"\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
|
|
2534
2688
|
)
|
|
2535
2689
|
self._run_tests(options)
|
|
2690
|
+
self._run_comprehensive_quality_checks(options)
|
|
2536
2691
|
self._bump_version(options)
|
|
2537
2692
|
self._publish_project(options)
|
|
2538
2693
|
self._commit_and_push(options)
|
|
@@ -2568,6 +2723,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2568
2723
|
"\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
|
|
2569
2724
|
)
|
|
2570
2725
|
await self._run_tests_async(options)
|
|
2726
|
+
await self._run_comprehensive_quality_checks_async(options)
|
|
2571
2727
|
self._bump_version(options)
|
|
2572
2728
|
self._publish_project(options)
|
|
2573
2729
|
self._commit_and_push(options)
|
crackerjack/interactive.py
CHANGED
|
@@ -85,7 +85,7 @@ class WorkflowManager:
|
|
|
85
85
|
def add_task(
|
|
86
86
|
self, name: str, description: str, dependencies: list[str] | None = None
|
|
87
87
|
) -> Task:
|
|
88
|
-
dep_tasks = []
|
|
88
|
+
dep_tasks: list[Task] = []
|
|
89
89
|
if dependencies:
|
|
90
90
|
for dep_name in dependencies:
|
|
91
91
|
if dep_name not in self.tasks:
|
crackerjack/py313.py
CHANGED
|
@@ -109,7 +109,7 @@ def process_hook_results[T, R](
|
|
|
109
109
|
success_handler: typing.Callable[[T], R],
|
|
110
110
|
failure_handler: typing.Callable[[T], R],
|
|
111
111
|
) -> list[R]:
|
|
112
|
-
processed_results = []
|
|
112
|
+
processed_results: list[R] = []
|
|
113
113
|
for result in results:
|
|
114
114
|
if isinstance(result, dict) and result.get("status") == HookStatus.SUCCESS:
|
|
115
115
|
processed_results.append(success_handler(result))
|
|
@@ -156,7 +156,7 @@ class EnhancedCommandRunner:
|
|
|
156
156
|
|
|
157
157
|
def clean_python_code(code: str) -> str:
|
|
158
158
|
lines = code.splitlines()
|
|
159
|
-
cleaned_lines = []
|
|
159
|
+
cleaned_lines: list[str] = []
|
|
160
160
|
for line in lines:
|
|
161
161
|
match line.strip():
|
|
162
162
|
case "":
|
crackerjack/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "crackerjack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.27.0"
|
|
8
8
|
description = "Crackerjack: code quality toolkit"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [
|
|
@@ -81,19 +81,11 @@ output-format = "full"
|
|
|
81
81
|
format.docstring-code-format = true
|
|
82
82
|
lint.extend-select = [
|
|
83
83
|
"C901",
|
|
84
|
-
"D",
|
|
85
84
|
"F", # pyflakes
|
|
86
85
|
"I",
|
|
87
86
|
"UP", # pyupgrade (includes F-string conversion)
|
|
88
87
|
]
|
|
89
88
|
lint.ignore = [
|
|
90
|
-
"D100", # Missing docstring in public module - can be safely enabled
|
|
91
|
-
"D101", # Missing docstring in public class - conflicts with code cleaning
|
|
92
|
-
"D102", # Missing docstring in public method - conflicts with code cleaning
|
|
93
|
-
"D103", # Missing docstring in public function - conflicts with code cleaning
|
|
94
|
-
"D104", # Missing docstring in public package - package level docs
|
|
95
|
-
"D105", # Missing docstring in magic method - conflicts with code cleaning
|
|
96
|
-
"D107", # Missing docstring in __init__ - conflicts with code cleaning
|
|
97
89
|
"E402", # Module level import not at top - sometimes necessary
|
|
98
90
|
"F821", # Undefined name - can be resolved with proper imports
|
|
99
91
|
]
|
|
@@ -103,7 +95,6 @@ lint.isort.no-lines-before = [
|
|
|
103
95
|
"first-party",
|
|
104
96
|
]
|
|
105
97
|
lint.mccabe.max-complexity = 13
|
|
106
|
-
lint.pydocstyle.convention = "google"
|
|
107
98
|
|
|
108
99
|
[tool.codespell]
|
|
109
100
|
skip = "*/data/*"
|
|
@@ -271,15 +262,6 @@ exclude_dirs = [
|
|
|
271
262
|
"tests/data", # Test data might contain examples
|
|
272
263
|
]
|
|
273
264
|
|
|
274
|
-
# Enhanced documentation quality
|
|
275
|
-
|
|
276
|
-
[tool.pydocstyle]
|
|
277
|
-
convention = "google"
|
|
278
|
-
add-ignore = [
|
|
279
|
-
"D100", # Missing docstring in public module
|
|
280
|
-
"D104", # Missing docstring in public package
|
|
281
|
-
]
|
|
282
|
-
|
|
283
265
|
# Autotyping optimization
|
|
284
266
|
|
|
285
267
|
[tool.autotyping]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
crackerjack/.gitignore,sha256=_d0WeGfNZQeCgzJXmjL9LKf-6jjytBulH-ZfWiIOZWI,453
|
|
2
|
+
crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
|
|
3
|
+
crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
|
|
4
|
+
crackerjack/.pre-commit-config-ai.yaml,sha256=-8WIT-6l6crGnQBlX-z3G6-3mKUsBWsURRyeti1ySmI,4267
|
|
5
|
+
crackerjack/.pre-commit-config.yaml,sha256=UgPPAC7O_npBLGJkC_v_2YQUSEIOgsBOXzZjyW2hNvs,2987
|
|
6
|
+
crackerjack/__init__.py,sha256=8tBSPAru_YDuPpjz05cL7pNbZjYFoRT_agGd_FWa3gY,839
|
|
7
|
+
crackerjack/__main__.py,sha256=p7S0GftrU9BHtyqT8q931UtEBoz3n8vPcT1R0OJnS1A,7073
|
|
8
|
+
crackerjack/crackerjack.py,sha256=R_fL_XItW_NNxEH3tj9buAmAPJ1CsdFNZd4KD7LJsS4,108843
|
|
9
|
+
crackerjack/errors.py,sha256=Wcv0rXfzV9pHOoXYrhQEjyJd4kUUBbdiY-5M9nI8pDw,4050
|
|
10
|
+
crackerjack/interactive.py,sha256=jnf3klyYFvuQ3u_iVVPshPW1LISfU1VXTOiczTWLxys,16138
|
|
11
|
+
crackerjack/py313.py,sha256=LCWcFNhF6QvPksobyUtxbnmlKosM03xDMb55yTlz6Ow,5910
|
|
12
|
+
crackerjack/pyproject.toml,sha256=0DKrCSAADU23POMqkig80AJKzxnZXleZbLZ2EiTzPHg,6870
|
|
13
|
+
crackerjack-0.27.1.dist-info/METADATA,sha256=29r5DGft8pRQP0QauVaNlbwhKxDYdHiFJnO6IpgL2Vw,28788
|
|
14
|
+
crackerjack-0.27.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
crackerjack-0.27.1.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
|
|
16
|
+
crackerjack-0.27.1.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
crackerjack/.gitignore,sha256=_d0WeGfNZQeCgzJXmjL9LKf-6jjytBulH-ZfWiIOZWI,453
|
|
2
|
-
crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
|
|
3
|
-
crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
|
|
4
|
-
crackerjack/.pre-commit-config-ai.yaml,sha256=-8WIT-6l6crGnQBlX-z3G6-3mKUsBWsURRyeti1ySmI,4267
|
|
5
|
-
crackerjack/.pre-commit-config.yaml,sha256=VcK0a32l_OjR-b9hkfXS-eSQORBxsS0QyaVQXPz8jOc,3345
|
|
6
|
-
crackerjack/__init__.py,sha256=8tBSPAru_YDuPpjz05cL7pNbZjYFoRT_agGd_FWa3gY,839
|
|
7
|
-
crackerjack/__main__.py,sha256=4BxL6-o1wrfouAgcXd91eInS2FJiEyhT3a6V5ZfnBWU,7082
|
|
8
|
-
crackerjack/crackerjack.py,sha256=5JZKO_CjbTmiQAVDlso7XSkfTWbs-O80H_3UUEjs79o,102989
|
|
9
|
-
crackerjack/errors.py,sha256=Wcv0rXfzV9pHOoXYrhQEjyJd4kUUBbdiY-5M9nI8pDw,4050
|
|
10
|
-
crackerjack/interactive.py,sha256=pFItgRUyjOakABLCRz6nIp6_Ycx2LBSeojpYNiTelv0,16126
|
|
11
|
-
crackerjack/py313.py,sha256=buYE7LO11Q64ffowEhTZRFQoAGj_8sg3DTlZuv8M9eo,5890
|
|
12
|
-
crackerjack/pyproject.toml,sha256=7lWQyzNq4anXZVpfDmIRkJZc3pXso-LbCHAI4rKnZIw,7647
|
|
13
|
-
crackerjack-0.27.0.dist-info/METADATA,sha256=6HxK2bFU_5w-0-Uxr0LVWaCISHmUOSpzsDiV2XITUes,28788
|
|
14
|
-
crackerjack-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
-
crackerjack-0.27.0.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
|
|
16
|
-
crackerjack-0.27.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|