crackerjack 0.27.0__py3-none-any.whl → 0.27.3__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/.gitignore +3 -0
- crackerjack/.pre-commit-config.yaml +0 -12
- crackerjack/__main__.py +3 -2
- crackerjack/crackerjack.py +360 -99
- crackerjack/interactive.py +1 -1
- crackerjack/py313.py +7 -3
- crackerjack/pyproject.toml +1 -19
- {crackerjack-0.27.0.dist-info → crackerjack-0.27.3.dist-info}/METADATA +1 -1
- crackerjack-0.27.3.dist-info/RECORD +16 -0
- crackerjack-0.27.0.dist-info/RECORD +0 -16
- {crackerjack-0.27.0.dist-info → crackerjack-0.27.3.dist-info}/WHEEL +0 -0
- {crackerjack-0.27.0.dist-info → crackerjack-0.27.3.dist-info}/licenses/LICENSE +0 -0
crackerjack/.gitignore
CHANGED
|
@@ -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):
|
|
@@ -539,9 +540,15 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
539
540
|
|
|
540
541
|
def process_lines():
|
|
541
542
|
lines = code.split("\n")
|
|
542
|
-
function_tracker
|
|
543
|
-
|
|
544
|
-
|
|
543
|
+
function_tracker: dict[str, t.Any] = {
|
|
544
|
+
"in_function": False,
|
|
545
|
+
"function_indent": 0,
|
|
546
|
+
}
|
|
547
|
+
import_tracker: dict[str, t.Any] = {
|
|
548
|
+
"in_imports": False,
|
|
549
|
+
"last_import_type": None,
|
|
550
|
+
}
|
|
551
|
+
previous_lines: list[str] = []
|
|
545
552
|
for i, line in enumerate(lines):
|
|
546
553
|
line = line.rstrip()
|
|
547
554
|
stripped_line = line.lstrip()
|
|
@@ -854,9 +861,9 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
854
861
|
from crackerjack.errors import ExecutionError, handle_error
|
|
855
862
|
|
|
856
863
|
try:
|
|
857
|
-
async with aiofiles.open(file_path, encoding="utf-8") as f:
|
|
858
|
-
code = await f.read()
|
|
859
|
-
original_code = code
|
|
864
|
+
async with aiofiles.open(file_path, encoding="utf-8") as f: # type: ignore[misc]
|
|
865
|
+
code: str = await f.read() # type: ignore[misc]
|
|
866
|
+
original_code: str = code
|
|
860
867
|
cleaning_failed = False
|
|
861
868
|
try:
|
|
862
869
|
code = self.remove_line_comments_streaming(code)
|
|
@@ -890,8 +897,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
890
897
|
)
|
|
891
898
|
code = original_code
|
|
892
899
|
cleaning_failed = True
|
|
893
|
-
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
894
|
-
await f.write(code)
|
|
900
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f: # type: ignore[misc]
|
|
901
|
+
await f.write(code) # type: ignore[misc]
|
|
895
902
|
if cleaning_failed:
|
|
896
903
|
self.console.print(
|
|
897
904
|
f"[bold yellow]⚡ Partially cleaned:[/bold yellow] [dim bright_white]{file_path}[/dim bright_white]"
|
|
@@ -965,8 +972,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
965
972
|
suffix=".py", mode="w+", delete=False
|
|
966
973
|
) as temp:
|
|
967
974
|
temp_path = Path(temp.name)
|
|
968
|
-
async with aiofiles.open(temp_path, "w", encoding="utf-8") as f:
|
|
969
|
-
await f.write(code)
|
|
975
|
+
async with aiofiles.open(temp_path, "w", encoding="utf-8") as f: # type: ignore[misc]
|
|
976
|
+
await f.write(code) # type: ignore[misc]
|
|
970
977
|
try:
|
|
971
978
|
proc = await asyncio.create_subprocess_exec(
|
|
972
979
|
"uv",
|
|
@@ -977,10 +984,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
977
984
|
stdout=asyncio.subprocess.PIPE,
|
|
978
985
|
stderr=asyncio.subprocess.PIPE,
|
|
979
986
|
)
|
|
980
|
-
|
|
987
|
+
_, stderr = await proc.communicate()
|
|
981
988
|
if proc.returncode == 0:
|
|
982
|
-
async with aiofiles.open(temp_path, encoding="utf-8") as f:
|
|
983
|
-
formatted_code = await f.read()
|
|
989
|
+
async with aiofiles.open(temp_path, encoding="utf-8") as f: # type: ignore[misc]
|
|
990
|
+
formatted_code = await f.read() # type: ignore[misc]
|
|
984
991
|
else:
|
|
985
992
|
self.console.print(
|
|
986
993
|
f"[bold bright_yellow]⚠️ Warning: Ruff format failed with return code {proc.returncode}[/bold bright_yellow]"
|
|
@@ -1025,7 +1032,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
|
1025
1032
|
return code
|
|
1026
1033
|
|
|
1027
1034
|
async def _cleanup_cache_directories_async(self, pkg_dir: Path) -> None:
|
|
1028
|
-
def cleanup_sync():
|
|
1035
|
+
def cleanup_sync() -> None:
|
|
1029
1036
|
with suppress(PermissionError, OSError):
|
|
1030
1037
|
pycache_dir = pkg_dir / "__pycache__"
|
|
1031
1038
|
if pycache_dir.exists():
|
|
@@ -1114,7 +1121,9 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1114
1121
|
) -> None:
|
|
1115
1122
|
for setting, value in our_config.items():
|
|
1116
1123
|
if isinstance(value, dict):
|
|
1117
|
-
self._merge_nested_config(
|
|
1124
|
+
self._merge_nested_config(
|
|
1125
|
+
setting, t.cast(dict[str, t.Any], value), pkg_config
|
|
1126
|
+
)
|
|
1118
1127
|
else:
|
|
1119
1128
|
self._merge_direct_config(setting, value, pkg_config)
|
|
1120
1129
|
|
|
@@ -1133,11 +1142,13 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1133
1142
|
self, key: str, value: t.Any, nested_config: dict[str, t.Any]
|
|
1134
1143
|
) -> None:
|
|
1135
1144
|
if isinstance(value, str | list) and "crackerjack" in str(value):
|
|
1136
|
-
nested_config[key] = self.swap_package_name(value)
|
|
1145
|
+
nested_config[key] = self.swap_package_name(t.cast(str | list[str], value))
|
|
1137
1146
|
elif self._is_mergeable_list(key, value):
|
|
1138
1147
|
existing = nested_config.get(key, [])
|
|
1139
1148
|
if isinstance(existing, list) and isinstance(value, list):
|
|
1140
|
-
nested_config[key] = list(
|
|
1149
|
+
nested_config[key] = list(
|
|
1150
|
+
set(t.cast(list[str], existing) + t.cast(list[str], value))
|
|
1151
|
+
)
|
|
1141
1152
|
else:
|
|
1142
1153
|
nested_config[key] = value
|
|
1143
1154
|
elif key not in nested_config:
|
|
@@ -1147,11 +1158,13 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1147
1158
|
self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
|
|
1148
1159
|
) -> None:
|
|
1149
1160
|
if isinstance(value, str | list) and "crackerjack" in str(value):
|
|
1150
|
-
pkg_config[setting] = self.swap_package_name(value)
|
|
1161
|
+
pkg_config[setting] = self.swap_package_name(t.cast(str | list[str], value))
|
|
1151
1162
|
elif self._is_mergeable_list(setting, value):
|
|
1152
1163
|
existing = pkg_config.get(setting, [])
|
|
1153
1164
|
if isinstance(existing, list) and isinstance(value, list):
|
|
1154
|
-
pkg_config[setting] = list(
|
|
1165
|
+
pkg_config[setting] = list(
|
|
1166
|
+
set(t.cast(list[str], existing) + t.cast(list[str], value))
|
|
1167
|
+
)
|
|
1155
1168
|
else:
|
|
1156
1169
|
pkg_config[setting] = value
|
|
1157
1170
|
elif setting not in pkg_config:
|
|
@@ -1171,7 +1184,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1171
1184
|
) -> None:
|
|
1172
1185
|
python_version_pattern = "\\s*W*(\\d\\.\\d*)"
|
|
1173
1186
|
requires_python = our_toml_config.get("project", {}).get("requires-python", "")
|
|
1174
|
-
classifiers = []
|
|
1187
|
+
classifiers: list[str] = []
|
|
1175
1188
|
for classifier in pkg_toml_config.get("project", {}).get("classifiers", []):
|
|
1176
1189
|
classifier = re.sub(
|
|
1177
1190
|
python_version_pattern, f" {self.python_version}", classifier
|
|
@@ -1186,7 +1199,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1186
1199
|
self.pkg_toml_path.write_text(dumps(pkg_toml_config))
|
|
1187
1200
|
|
|
1188
1201
|
def copy_configs(self) -> None:
|
|
1189
|
-
configs_to_add = []
|
|
1202
|
+
configs_to_add: list[str] = []
|
|
1190
1203
|
for config in config_files:
|
|
1191
1204
|
config_path = self.our_path / config
|
|
1192
1205
|
pkg_config_path = self.pkg_path / config
|
|
@@ -1238,7 +1251,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1238
1251
|
len(py_files) + len(js_files) + len(yaml_files) + len(md_files)
|
|
1239
1252
|
)
|
|
1240
1253
|
total_size = 0
|
|
1241
|
-
for files in
|
|
1254
|
+
for files in (py_files, js_files, yaml_files, md_files):
|
|
1242
1255
|
for file_path in files:
|
|
1243
1256
|
try:
|
|
1244
1257
|
total_size += file_path.stat().st_size
|
|
@@ -1395,7 +1408,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1395
1408
|
return hook_results
|
|
1396
1409
|
|
|
1397
1410
|
def _parse_hook_output(self, stdout: str, stderr: str) -> list[HookResult]:
|
|
1398
|
-
hook_results = []
|
|
1411
|
+
hook_results: list[HookResult] = []
|
|
1399
1412
|
lines = stdout.split("\n")
|
|
1400
1413
|
for line in lines:
|
|
1401
1414
|
if "..." in line and (
|
|
@@ -1482,7 +1495,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1482
1495
|
def _generate_optimization_suggestions(
|
|
1483
1496
|
self, hook_results: list[HookResult]
|
|
1484
1497
|
) -> list[str]:
|
|
1485
|
-
suggestions = []
|
|
1498
|
+
suggestions: list[str] = []
|
|
1486
1499
|
|
|
1487
1500
|
for hook in hook_results:
|
|
1488
1501
|
if hook.duration > 5.0:
|
|
@@ -1578,35 +1591,42 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1578
1591
|
def _parse_refurb_results(self) -> dict[str, t.Any]:
|
|
1579
1592
|
return {"suggestions": 0, "patterns_modernized": []}
|
|
1580
1593
|
|
|
1594
|
+
def _parse_complexity_list(
|
|
1595
|
+
self, complexity_data: list[dict[str, t.Any]]
|
|
1596
|
+
) -> dict[str, t.Any]:
|
|
1597
|
+
if not complexity_data:
|
|
1598
|
+
return {
|
|
1599
|
+
"average_complexity": 0,
|
|
1600
|
+
"max_complexity": 0,
|
|
1601
|
+
"total_functions": 0,
|
|
1602
|
+
}
|
|
1603
|
+
complexities = [item.get("complexity", 0) for item in complexity_data]
|
|
1604
|
+
return {
|
|
1605
|
+
"average_complexity": sum(complexities) / len(complexities)
|
|
1606
|
+
if complexities
|
|
1607
|
+
else 0,
|
|
1608
|
+
"max_complexity": max(complexities) if complexities else 0,
|
|
1609
|
+
"total_functions": len(complexities),
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
def _parse_complexity_dict(
|
|
1613
|
+
self, complexity_data: dict[str, t.Any]
|
|
1614
|
+
) -> dict[str, t.Any]:
|
|
1615
|
+
return {
|
|
1616
|
+
"average_complexity": complexity_data.get("average", 0),
|
|
1617
|
+
"max_complexity": complexity_data.get("max", 0),
|
|
1618
|
+
"total_functions": complexity_data.get("total", 0),
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1581
1621
|
def _parse_complexity_results(self) -> dict[str, t.Any]:
|
|
1582
1622
|
try:
|
|
1583
1623
|
with open("complexipy.json", encoding="utf-8") as f:
|
|
1584
1624
|
complexity_data = json.load(f)
|
|
1585
1625
|
if isinstance(complexity_data, list):
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
}
|
|
1626
|
+
return self._parse_complexity_list(
|
|
1627
|
+
t.cast(list[dict[str, t.Any]], complexity_data)
|
|
1628
|
+
)
|
|
1629
|
+
return self._parse_complexity_dict(complexity_data)
|
|
1610
1630
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
1611
1631
|
return {"status": "complexity_analysis_not_available"}
|
|
1612
1632
|
|
|
@@ -1653,7 +1673,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1653
1673
|
return 95.0
|
|
1654
1674
|
|
|
1655
1675
|
def _generate_quality_recommendations(self) -> list[str]:
|
|
1656
|
-
recommendations = []
|
|
1676
|
+
recommendations: list[str] = []
|
|
1657
1677
|
recommendations.extend(
|
|
1658
1678
|
[
|
|
1659
1679
|
"Consider adding more integration tests",
|
|
@@ -1749,22 +1769,20 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1749
1769
|
return "unknown"
|
|
1750
1770
|
|
|
1751
1771
|
def _analyze_directory_structure(self) -> dict[str, t.Any]:
|
|
1752
|
-
directories = [
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
)
|
|
1772
|
+
directories = [
|
|
1773
|
+
{
|
|
1774
|
+
"name": item.name,
|
|
1775
|
+
"type": self._classify_directory(item),
|
|
1776
|
+
"file_count": len(list(item.rglob("*"))),
|
|
1777
|
+
}
|
|
1778
|
+
for item in self.pkg_path.iterdir()
|
|
1779
|
+
if item.is_dir()
|
|
1780
|
+
and not item.name.startswith((".git", "__pycache__", ".venv"))
|
|
1781
|
+
]
|
|
1764
1782
|
return {"directories": directories, "total_directories": len(directories)}
|
|
1765
1783
|
|
|
1766
1784
|
def _analyze_file_distribution(self) -> dict[str, t.Any]:
|
|
1767
|
-
file_types = {}
|
|
1785
|
+
file_types: dict[str, int] = {}
|
|
1768
1786
|
total_files = 0
|
|
1769
1787
|
for file_path in self.pkg_path.rglob("*"):
|
|
1770
1788
|
if file_path.is_file() and not str(file_path).startswith(
|
|
@@ -1778,17 +1796,15 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1778
1796
|
|
|
1779
1797
|
def _analyze_dependencies(self) -> dict[str, t.Any]:
|
|
1780
1798
|
deps = {"status": "analysis_not_implemented"}
|
|
1781
|
-
|
|
1799
|
+
with suppress(Exception):
|
|
1782
1800
|
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
1783
1801
|
if pyproject_path.exists():
|
|
1784
1802
|
pyproject_path.read_text(encoding="utf-8")
|
|
1785
1803
|
deps = {"source": "pyproject.toml", "status": "detected"}
|
|
1786
|
-
except Exception:
|
|
1787
|
-
pass
|
|
1788
1804
|
return deps
|
|
1789
1805
|
|
|
1790
1806
|
def _analyze_configuration_files(self) -> list[str]:
|
|
1791
|
-
config_files = []
|
|
1807
|
+
config_files: list[str] = []
|
|
1792
1808
|
config_patterns = ["*.toml", "*.yaml", "*.yml", "*.ini", "*.cfg", ".env*"]
|
|
1793
1809
|
for pattern in config_patterns:
|
|
1794
1810
|
config_files.extend([f.name for f in self.pkg_path.glob(pattern)])
|
|
@@ -1859,8 +1875,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1859
1875
|
return "hidden"
|
|
1860
1876
|
elif (directory / "__init__.py").exists():
|
|
1861
1877
|
return "python_package"
|
|
1862
|
-
|
|
1863
|
-
return "general"
|
|
1878
|
+
return "general"
|
|
1864
1879
|
|
|
1865
1880
|
def _collect_environment_info(self) -> dict[str, t.Any]:
|
|
1866
1881
|
return {
|
|
@@ -1871,7 +1886,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1871
1886
|
}
|
|
1872
1887
|
|
|
1873
1888
|
def _identify_common_issues(self) -> list[str]:
|
|
1874
|
-
issues = []
|
|
1889
|
+
issues: list[str] = []
|
|
1875
1890
|
if not (self.pkg_path / "pyproject.toml").exists():
|
|
1876
1891
|
issues.append("Missing pyproject.toml configuration")
|
|
1877
1892
|
if not (self.pkg_path / ".gitignore").exists():
|
|
@@ -2048,6 +2063,58 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2048
2063
|
|
|
2049
2064
|
return hook_results
|
|
2050
2065
|
|
|
2066
|
+
def _get_analysis_files(self) -> list[str]:
|
|
2067
|
+
analysis_files: list[str] = []
|
|
2068
|
+
if (
|
|
2069
|
+
hasattr(self, "options")
|
|
2070
|
+
and self.options
|
|
2071
|
+
and getattr(self.options, "ai_agent", False)
|
|
2072
|
+
):
|
|
2073
|
+
analysis_files.extend(
|
|
2074
|
+
[
|
|
2075
|
+
"test-results.xml",
|
|
2076
|
+
"coverage.json",
|
|
2077
|
+
"benchmark.json",
|
|
2078
|
+
"ai-agent-summary.json",
|
|
2079
|
+
]
|
|
2080
|
+
)
|
|
2081
|
+
return analysis_files
|
|
2082
|
+
|
|
2083
|
+
def _generate_analysis_files(self, hook_results: list[HookResult]) -> None:
|
|
2084
|
+
if not (
|
|
2085
|
+
hasattr(self, "options")
|
|
2086
|
+
and self.options
|
|
2087
|
+
and getattr(self.options, "ai_agent", False)
|
|
2088
|
+
):
|
|
2089
|
+
return
|
|
2090
|
+
try:
|
|
2091
|
+
import json
|
|
2092
|
+
|
|
2093
|
+
summary = {
|
|
2094
|
+
"status": "success"
|
|
2095
|
+
if all(hr.status == "Passed" for hr in hook_results)
|
|
2096
|
+
else "failed",
|
|
2097
|
+
"hook_results": [
|
|
2098
|
+
{
|
|
2099
|
+
"name": hr.name,
|
|
2100
|
+
"status": hr.status,
|
|
2101
|
+
"duration": hr.duration,
|
|
2102
|
+
"issues": hr.issues_found
|
|
2103
|
+
if hasattr(hr, "issues_found")
|
|
2104
|
+
else [],
|
|
2105
|
+
}
|
|
2106
|
+
for hr in hook_results
|
|
2107
|
+
],
|
|
2108
|
+
"total_duration": sum(hr.duration for hr in hook_results),
|
|
2109
|
+
"files_analyzed": len(hook_results),
|
|
2110
|
+
}
|
|
2111
|
+
with open("ai-agent-summary.json", "w") as f:
|
|
2112
|
+
json.dump(summary, f, indent=2)
|
|
2113
|
+
except Exception as e:
|
|
2114
|
+
self.console.print(
|
|
2115
|
+
f"[yellow]Warning: Failed to generate AI summary: {e}[/yellow]"
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2051
2118
|
|
|
2052
2119
|
class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
2053
2120
|
our_path: Path = Path(__file__).parent
|
|
@@ -2062,11 +2129,13 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2062
2129
|
project_manager: ProjectManager | None = None
|
|
2063
2130
|
_file_cache: dict[str, list[Path]] = {}
|
|
2064
2131
|
_file_cache_with_mtime: dict[str, tuple[float, list[Path]]] = {}
|
|
2132
|
+
_state_file: Path = Path(".crackerjack-state")
|
|
2065
2133
|
|
|
2066
2134
|
def __init__(self, **data: t.Any) -> None:
|
|
2067
2135
|
super().__init__(**data)
|
|
2068
2136
|
self._file_cache = {}
|
|
2069
2137
|
self._file_cache_with_mtime = {}
|
|
2138
|
+
self._state_file = Path(".crackerjack-state")
|
|
2070
2139
|
self.code_cleaner = CodeCleaner(console=self.console)
|
|
2071
2140
|
self.config_manager = ConfigManager(
|
|
2072
2141
|
our_path=self.our_path,
|
|
@@ -2087,6 +2156,73 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2087
2156
|
dry_run=self.dry_run,
|
|
2088
2157
|
)
|
|
2089
2158
|
|
|
2159
|
+
def _read_state(self) -> dict[str, t.Any]:
|
|
2160
|
+
import json
|
|
2161
|
+
|
|
2162
|
+
if self._state_file.exists():
|
|
2163
|
+
try:
|
|
2164
|
+
return json.loads(self._state_file.read_text(encoding="utf-8"))
|
|
2165
|
+
except (json.JSONDecodeError, OSError):
|
|
2166
|
+
return {}
|
|
2167
|
+
return {}
|
|
2168
|
+
|
|
2169
|
+
def _write_state(self, state: dict[str, t.Any]) -> None:
|
|
2170
|
+
from contextlib import suppress
|
|
2171
|
+
|
|
2172
|
+
with suppress(OSError):
|
|
2173
|
+
import json
|
|
2174
|
+
|
|
2175
|
+
self._state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
2176
|
+
|
|
2177
|
+
def _clear_state(self) -> None:
|
|
2178
|
+
if self._state_file.exists():
|
|
2179
|
+
from contextlib import suppress
|
|
2180
|
+
|
|
2181
|
+
with suppress(OSError):
|
|
2182
|
+
self._state_file.unlink()
|
|
2183
|
+
|
|
2184
|
+
def _has_version_been_bumped(self, version_type: str) -> bool:
|
|
2185
|
+
state = self._read_state()
|
|
2186
|
+
current_version = self._get_current_version()
|
|
2187
|
+
last_bumped_version = state.get("last_bumped_version")
|
|
2188
|
+
last_bump_type = state.get("last_bump_type")
|
|
2189
|
+
|
|
2190
|
+
return (
|
|
2191
|
+
last_bumped_version == current_version
|
|
2192
|
+
and last_bump_type == version_type
|
|
2193
|
+
and not state.get("publish_completed", False)
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
def _mark_version_bumped(self, version_type: str) -> None:
|
|
2197
|
+
current_version = self._get_current_version()
|
|
2198
|
+
state = self._read_state()
|
|
2199
|
+
state.update(
|
|
2200
|
+
{
|
|
2201
|
+
"last_bumped_version": current_version,
|
|
2202
|
+
"last_bump_type": version_type,
|
|
2203
|
+
"publish_completed": False,
|
|
2204
|
+
}
|
|
2205
|
+
)
|
|
2206
|
+
self._write_state(state)
|
|
2207
|
+
|
|
2208
|
+
def _mark_publish_completed(self) -> None:
|
|
2209
|
+
state = self._read_state()
|
|
2210
|
+
state["publish_completed"] = True
|
|
2211
|
+
self._write_state(state)
|
|
2212
|
+
|
|
2213
|
+
def _get_current_version(self) -> str:
|
|
2214
|
+
from contextlib import suppress
|
|
2215
|
+
|
|
2216
|
+
with suppress(Exception):
|
|
2217
|
+
import tomllib
|
|
2218
|
+
|
|
2219
|
+
pyproject_path = Path("pyproject.toml")
|
|
2220
|
+
if pyproject_path.exists():
|
|
2221
|
+
with pyproject_path.open("rb") as f:
|
|
2222
|
+
data = tomllib.load(f)
|
|
2223
|
+
return data.get("project", {}).get("version", "unknown")
|
|
2224
|
+
return "unknown"
|
|
2225
|
+
|
|
2090
2226
|
def _setup_package(self) -> None:
|
|
2091
2227
|
self.pkg_name = self.pkg_path.stem.lower().replace("-", "_")
|
|
2092
2228
|
self.pkg_dir = self.pkg_path / self.pkg_name
|
|
@@ -2096,11 +2232,14 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2096
2232
|
"[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]"
|
|
2097
2233
|
)
|
|
2098
2234
|
self.console.print("-" * 80 + "\n")
|
|
2235
|
+
assert self.config_manager is not None
|
|
2236
|
+
assert self.project_manager is not None
|
|
2099
2237
|
self.config_manager.pkg_name = self.pkg_name
|
|
2100
2238
|
self.project_manager.pkg_name = self.pkg_name
|
|
2101
2239
|
self.project_manager.pkg_dir = self.pkg_dir
|
|
2102
2240
|
|
|
2103
2241
|
def _update_project(self, options: t.Any) -> None:
|
|
2242
|
+
assert self.project_manager is not None
|
|
2104
2243
|
if not options.no_config_updates:
|
|
2105
2244
|
self.project_manager.update_pkg_configs()
|
|
2106
2245
|
result: CompletedProcess[str] = self.execute_command(
|
|
@@ -2123,6 +2262,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2123
2262
|
self.execute_command(update_cmd)
|
|
2124
2263
|
|
|
2125
2264
|
def _clean_project(self, options: t.Any) -> None:
|
|
2265
|
+
assert self.code_cleaner is not None
|
|
2126
2266
|
if options.clean:
|
|
2127
2267
|
if self.pkg_dir:
|
|
2128
2268
|
self.console.print("\n" + "-" * 80)
|
|
@@ -2142,6 +2282,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2142
2282
|
self.code_cleaner.clean_files(tests_dir)
|
|
2143
2283
|
|
|
2144
2284
|
async def _clean_project_async(self, options: t.Any) -> None:
|
|
2285
|
+
assert self.code_cleaner is not None
|
|
2145
2286
|
if options.clean:
|
|
2146
2287
|
if self.pkg_dir:
|
|
2147
2288
|
self.console.print("\n" + "-" * 80)
|
|
@@ -2309,31 +2450,38 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2309
2450
|
except (OSError, PermissionError):
|
|
2310
2451
|
return "medium"
|
|
2311
2452
|
|
|
2453
|
+
def _calculate_test_metrics(self, test_files: list[Path]) -> tuple[int, int]:
|
|
2454
|
+
total_test_size = 0
|
|
2455
|
+
slow_tests = 0
|
|
2456
|
+
for test_file in test_files:
|
|
2457
|
+
try:
|
|
2458
|
+
size = test_file.stat().st_size
|
|
2459
|
+
total_test_size += size
|
|
2460
|
+
if size > 30_000 or "integration" in test_file.name.lower():
|
|
2461
|
+
slow_tests += 1
|
|
2462
|
+
except (OSError, PermissionError):
|
|
2463
|
+
continue
|
|
2464
|
+
return total_test_size, slow_tests
|
|
2465
|
+
|
|
2466
|
+
def _determine_test_complexity(
|
|
2467
|
+
self, test_count: int, avg_size: float, slow_ratio: float
|
|
2468
|
+
) -> str:
|
|
2469
|
+
if test_count > 100 or avg_size > 25_000 or slow_ratio > 0.4:
|
|
2470
|
+
return "high"
|
|
2471
|
+
elif test_count > 50 or avg_size > 15_000 or slow_ratio > 0.2:
|
|
2472
|
+
return "medium"
|
|
2473
|
+
return "low"
|
|
2474
|
+
|
|
2312
2475
|
def _analyze_test_workload(self) -> dict[str, t.Any]:
|
|
2313
2476
|
try:
|
|
2314
2477
|
test_files = self._get_cached_files_with_mtime("test_*.py")
|
|
2315
2478
|
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
|
|
2479
|
+
total_test_size, slow_tests = self._calculate_test_metrics(test_files)
|
|
2326
2480
|
avg_test_size = total_test_size / len(test_files) if test_files else 0
|
|
2327
2481
|
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
|
-
|
|
2482
|
+
complexity = self._determine_test_complexity(
|
|
2483
|
+
len(test_files), avg_test_size, slow_test_ratio
|
|
2484
|
+
)
|
|
2337
2485
|
return {
|
|
2338
2486
|
"total_files": len(py_files),
|
|
2339
2487
|
"test_files": len(test_files),
|
|
@@ -2354,8 +2502,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2354
2502
|
return min(cpu_count // 3, 2)
|
|
2355
2503
|
elif workload["complexity"] == "medium":
|
|
2356
2504
|
return min(cpu_count // 2, 4)
|
|
2357
|
-
|
|
2358
|
-
return min(cpu_count, 8)
|
|
2505
|
+
return min(cpu_count, 8)
|
|
2359
2506
|
|
|
2360
2507
|
def _print_ai_agent_files(self, options: t.Any) -> None:
|
|
2361
2508
|
if getattr(options, "ai_agent", False):
|
|
@@ -2422,12 +2569,20 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2422
2569
|
def _bump_version(self, options: OptionsProtocol) -> None:
|
|
2423
2570
|
for option in (options.publish, options.bump):
|
|
2424
2571
|
if option:
|
|
2572
|
+
version_type = str(option)
|
|
2573
|
+
if self._has_version_been_bumped(version_type):
|
|
2574
|
+
self.console.print("\n" + "-" * 80)
|
|
2575
|
+
self.console.print(
|
|
2576
|
+
f"[bold yellow]📦 VERSION[/bold yellow] [bold bright_white]Version already bumped ({version_type}), skipping to avoid duplicate bump[/bold bright_white]"
|
|
2577
|
+
)
|
|
2578
|
+
self.console.print("-" * 80 + "\n")
|
|
2579
|
+
return
|
|
2425
2580
|
self.console.print("\n" + "-" * 80)
|
|
2426
2581
|
self.console.print(
|
|
2427
2582
|
f"[bold bright_magenta]📦 VERSION[/bold bright_magenta] [bold bright_white]Bumping {option} version[/bold bright_white]"
|
|
2428
2583
|
)
|
|
2429
2584
|
self.console.print("-" * 80 + "\n")
|
|
2430
|
-
if
|
|
2585
|
+
if version_type in ("minor", "major"):
|
|
2431
2586
|
from rich.prompt import Confirm
|
|
2432
2587
|
|
|
2433
2588
|
if not Confirm.ask(
|
|
@@ -2439,6 +2594,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2439
2594
|
)
|
|
2440
2595
|
return
|
|
2441
2596
|
self.execute_command(["uv", "version", "--bump", option])
|
|
2597
|
+
self._mark_version_bumped(version_type)
|
|
2442
2598
|
break
|
|
2443
2599
|
|
|
2444
2600
|
def _publish_project(self, options: OptionsProtocol) -> None:
|
|
@@ -2458,7 +2614,18 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2458
2614
|
"[bold bright_red]❌ Build failed. Please fix errors.[/bold bright_red]"
|
|
2459
2615
|
)
|
|
2460
2616
|
raise SystemExit(1)
|
|
2461
|
-
|
|
2617
|
+
try:
|
|
2618
|
+
self.execute_command(["uv", "publish"])
|
|
2619
|
+
self._mark_publish_completed()
|
|
2620
|
+
self._clear_state()
|
|
2621
|
+
self.console.print(
|
|
2622
|
+
"\n[bold bright_green]✅ Package published successfully![/bold bright_green]"
|
|
2623
|
+
)
|
|
2624
|
+
except SystemExit:
|
|
2625
|
+
self.console.print(
|
|
2626
|
+
"\n[bold bright_red]❌ Publish failed. Run crackerjack again to retry publishing without re-bumping version.[/bold bright_red]"
|
|
2627
|
+
)
|
|
2628
|
+
raise
|
|
2462
2629
|
|
|
2463
2630
|
def _commit_and_push(self, options: OptionsProtocol) -> None:
|
|
2464
2631
|
if options.commit:
|
|
@@ -2507,7 +2674,98 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2507
2674
|
stderr.decode() if stderr else "",
|
|
2508
2675
|
)
|
|
2509
2676
|
|
|
2677
|
+
def _run_comprehensive_quality_checks(self, options: OptionsProtocol) -> None:
|
|
2678
|
+
if options.skip_hooks or (
|
|
2679
|
+
options.test
|
|
2680
|
+
and not any([options.publish, options.bump, options.commit, options.all])
|
|
2681
|
+
):
|
|
2682
|
+
return
|
|
2683
|
+
needs_comprehensive = any(
|
|
2684
|
+
[options.publish, options.bump, options.commit, options.all]
|
|
2685
|
+
)
|
|
2686
|
+
if not needs_comprehensive:
|
|
2687
|
+
return
|
|
2688
|
+
self.console.print("\n" + "-" * 80)
|
|
2689
|
+
self.console.print(
|
|
2690
|
+
"[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
|
|
2691
|
+
)
|
|
2692
|
+
self.console.print("-" * 80 + "\n")
|
|
2693
|
+
cmd = [
|
|
2694
|
+
"uv",
|
|
2695
|
+
"run",
|
|
2696
|
+
"pre-commit",
|
|
2697
|
+
"run",
|
|
2698
|
+
"--all-files",
|
|
2699
|
+
"--hook-stage=manual",
|
|
2700
|
+
"-c",
|
|
2701
|
+
".pre-commit-config.yaml",
|
|
2702
|
+
]
|
|
2703
|
+
result = self.execute_command(cmd)
|
|
2704
|
+
if result.returncode > 0:
|
|
2705
|
+
self.console.print(
|
|
2706
|
+
"\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
|
|
2707
|
+
)
|
|
2708
|
+
self.console.print(
|
|
2709
|
+
"\n[bold red]Cannot proceed with publishing/committing until all quality checks pass.[/bold red]\n"
|
|
2710
|
+
)
|
|
2711
|
+
raise SystemExit(1)
|
|
2712
|
+
else:
|
|
2713
|
+
self.console.print(
|
|
2714
|
+
"\n[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
|
|
2715
|
+
)
|
|
2716
|
+
|
|
2717
|
+
async def _run_comprehensive_quality_checks_async(
|
|
2718
|
+
self, options: OptionsProtocol
|
|
2719
|
+
) -> None:
|
|
2720
|
+
if options.skip_hooks or (
|
|
2721
|
+
options.test
|
|
2722
|
+
and not any([options.publish, options.bump, options.commit, options.all])
|
|
2723
|
+
):
|
|
2724
|
+
return
|
|
2725
|
+
|
|
2726
|
+
needs_comprehensive = any(
|
|
2727
|
+
[options.publish, options.bump, options.commit, options.all]
|
|
2728
|
+
)
|
|
2729
|
+
|
|
2730
|
+
if not needs_comprehensive:
|
|
2731
|
+
return
|
|
2732
|
+
|
|
2733
|
+
self.console.print("\n" + "-" * 80)
|
|
2734
|
+
self.console.print(
|
|
2735
|
+
"[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
|
|
2736
|
+
)
|
|
2737
|
+
self.console.print("-" * 80 + "\n")
|
|
2738
|
+
|
|
2739
|
+
cmd = [
|
|
2740
|
+
"uv",
|
|
2741
|
+
"run",
|
|
2742
|
+
"pre-commit",
|
|
2743
|
+
"run",
|
|
2744
|
+
"--all-files",
|
|
2745
|
+
"--hook-stage=manual",
|
|
2746
|
+
"-c",
|
|
2747
|
+
".pre-commit-config.yaml",
|
|
2748
|
+
]
|
|
2749
|
+
|
|
2750
|
+
result = await self.execute_command_async(cmd)
|
|
2751
|
+
|
|
2752
|
+
if result.returncode > 0:
|
|
2753
|
+
self.console.print(
|
|
2754
|
+
"\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
|
|
2755
|
+
)
|
|
2756
|
+
if result.stderr:
|
|
2757
|
+
self.console.print(f"[dim]Error details: {result.stderr}[/dim]")
|
|
2758
|
+
self.console.print(
|
|
2759
|
+
"\n[bold red]Cannot proceed with publishing/committing until all quality checks pass.[/bold red]\n"
|
|
2760
|
+
)
|
|
2761
|
+
raise SystemExit(1)
|
|
2762
|
+
else:
|
|
2763
|
+
self.console.print(
|
|
2764
|
+
"[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
|
|
2765
|
+
)
|
|
2766
|
+
|
|
2510
2767
|
def process(self, options: OptionsProtocol) -> None:
|
|
2768
|
+
assert self.project_manager is not None
|
|
2511
2769
|
self.console.print("\n" + "-" * 80)
|
|
2512
2770
|
self.console.print(
|
|
2513
2771
|
"[bold bright_cyan]⚒️ CRACKERJACKING[/bold bright_cyan] [bold bright_white]Starting workflow execution[/bold bright_white]"
|
|
@@ -2533,9 +2791,10 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2533
2791
|
"\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
|
|
2534
2792
|
)
|
|
2535
2793
|
self._run_tests(options)
|
|
2794
|
+
self._run_comprehensive_quality_checks(options)
|
|
2536
2795
|
self._bump_version(options)
|
|
2537
|
-
self._publish_project(options)
|
|
2538
2796
|
self._commit_and_push(options)
|
|
2797
|
+
self._publish_project(options)
|
|
2539
2798
|
self.console.print("\n" + "-" * 80)
|
|
2540
2799
|
self.console.print(
|
|
2541
2800
|
"[bold bright_green]✨ CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
|
|
@@ -2543,6 +2802,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2543
2802
|
self.console.print("-" * 80 + "\n")
|
|
2544
2803
|
|
|
2545
2804
|
async def process_async(self, options: OptionsProtocol) -> None:
|
|
2805
|
+
assert self.project_manager is not None
|
|
2546
2806
|
self.console.print("\n" + "-" * 80)
|
|
2547
2807
|
self.console.print(
|
|
2548
2808
|
"[bold bright_cyan]⚒️ CRACKERJACKING[/bold bright_cyan] [bold bright_white]Starting workflow execution (async optimized)[/bold bright_white]"
|
|
@@ -2568,9 +2828,10 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2568
2828
|
"\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
|
|
2569
2829
|
)
|
|
2570
2830
|
await self._run_tests_async(options)
|
|
2831
|
+
await self._run_comprehensive_quality_checks_async(options)
|
|
2571
2832
|
self._bump_version(options)
|
|
2572
|
-
self._publish_project(options)
|
|
2573
2833
|
self._commit_and_push(options)
|
|
2834
|
+
self._publish_project(options)
|
|
2574
2835
|
self.console.print("\n" + "-" * 80)
|
|
2575
2836
|
self.console.print(
|
|
2576
2837
|
"[bold bright_green]✨ CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
|
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,9 +109,13 @@ 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
|
-
if
|
|
114
|
+
if (
|
|
115
|
+
isinstance(result, dict)
|
|
116
|
+
and "status" in result
|
|
117
|
+
and result["status"] == HookStatus.SUCCESS
|
|
118
|
+
):
|
|
115
119
|
processed_results.append(success_handler(result))
|
|
116
120
|
else:
|
|
117
121
|
processed_results.append(failure_handler(result))
|
|
@@ -156,7 +160,7 @@ class EnhancedCommandRunner:
|
|
|
156
160
|
|
|
157
161
|
def clean_python_code(code: str) -> str:
|
|
158
162
|
lines = code.splitlines()
|
|
159
|
-
cleaned_lines = []
|
|
163
|
+
cleaned_lines: list[str] = []
|
|
160
164
|
for line in lines:
|
|
161
165
|
match line.strip():
|
|
162
166
|
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.2"
|
|
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=ELNEeIblDM1mKM-LJyj_iLcBzZJPbeJ89YXupvXuLug,498
|
|
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=dPQx3X6lQeGjDaguAJD-IB2knDxAp9payW7v7FBfctY,112736
|
|
9
|
+
crackerjack/errors.py,sha256=Wcv0rXfzV9pHOoXYrhQEjyJd4kUUBbdiY-5M9nI8pDw,4050
|
|
10
|
+
crackerjack/interactive.py,sha256=jnf3klyYFvuQ3u_iVVPshPW1LISfU1VXTOiczTWLxys,16138
|
|
11
|
+
crackerjack/py313.py,sha256=1imwWZUQwcZt09yIrnTSWr73ITTKH8yXlgIe2ESTeLA,5977
|
|
12
|
+
crackerjack/pyproject.toml,sha256=aFoPe_twLd57qFGjeQSYAyzVIi-e8XjT4W9Buyxy91A,6870
|
|
13
|
+
crackerjack-0.27.3.dist-info/METADATA,sha256=naNAMPq4s1yUE3x3kTnKW9dirlYRhDKFWQLgntVY30Q,28788
|
|
14
|
+
crackerjack-0.27.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
crackerjack-0.27.3.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
|
|
16
|
+
crackerjack-0.27.3.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
|