crackerjack 0.20.3__tar.gz → 0.20.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crackerjack-0.20.3 → crackerjack-0.20.10}/PKG-INFO +2 -1
- crackerjack-0.20.10/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
- crackerjack-0.20.10/crackerjack/.ruff_cache/0.11.13/4240757255861806333 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/__init__.py +0 -2
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/__main__.py +2 -11
- crackerjack-0.20.10/crackerjack/crackerjack.py +614 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/errors.py +0 -20
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/interactive.py +38 -121
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/py313.py +10 -50
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/pyproject.toml +3 -1
- {crackerjack-0.20.3 → crackerjack-0.20.10}/pyproject.toml +3 -1
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/conftest.py +2 -14
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_crackerjack.py +166 -212
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_crackerjack_runner.py +36 -56
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_errors.py +15 -28
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_interactive.py +4 -65
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_interactive_run.py +15 -24
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_main.py +0 -5
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_py313_advanced.py +10 -38
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_py313_features.py +1 -44
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_pytest_features.py +0 -4
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/test_structured_errors.py +83 -62
- crackerjack-0.20.3/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
- crackerjack-0.20.3/crackerjack/crackerjack.py +0 -1317
- {crackerjack-0.20.3 → crackerjack-0.20.10}/LICENSE +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/README.md +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.gitignore +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.libcst.codemod.yaml +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pdm.toml +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pre-commit-config.yaml +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pytest_cache/.gitignore +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pytest_cache/CACHEDIR.TAG +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pytest_cache/README.md +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pytest_cache/v/cache/nodeids +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.pytest_cache/v/cache/stepwise +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/.gitignore +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.11/18187162184424859798 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.12/16869036553936192448 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.12/4441409093023629623 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/crackerjack/.ruff_cache/CACHEDIR.TAG +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/TESTING.md +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/__init__.py +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/data/comments_sample.txt +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/data/docstrings_sample.txt +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/data/expected_comments_sample.txt +0 -0
- {crackerjack-0.20.3 → crackerjack-0.20.10}/tests/data/init.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: crackerjack
|
3
|
-
Version: 0.20.
|
3
|
+
Version: 0.20.10
|
4
4
|
Summary: Crackerjack: code quality toolkit
|
5
5
|
Keywords: bandit,black,creosote,mypy,pyright,pytest,refurb,ruff
|
6
6
|
Author-Email: lesleslie <les@wedgwoodwebworks.com>
|
@@ -23,6 +23,7 @@ Project-URL: homepage, https://github.com/lesleslie/crackerjack
|
|
23
23
|
Project-URL: repository, https://github.com/lesleslie/crackerjack
|
24
24
|
Requires-Python: >=3.13
|
25
25
|
Requires-Dist: autotyping>=24.9
|
26
|
+
Requires-Dist: keyring>=25.6
|
26
27
|
Requires-Dist: pdm>=2.24.2
|
27
28
|
Requires-Dist: pdm-bump>=0.9.12
|
28
29
|
Requires-Dist: pre-commit>=4.2
|
Binary file
|
Binary file
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import typing as t
|
2
|
-
|
3
2
|
from .crackerjack import Crackerjack, create_crackerjack_runner
|
4
3
|
from .errors import (
|
5
4
|
CleaningError,
|
@@ -22,7 +21,6 @@ try:
|
|
22
21
|
__version__ = version("crackerjack")
|
23
22
|
except (ImportError, ModuleNotFoundError):
|
24
23
|
__version__ = "0.19.8"
|
25
|
-
|
26
24
|
__all__: t.Sequence[str] = [
|
27
25
|
"create_crackerjack_runner",
|
28
26
|
"Crackerjack",
|
@@ -1,5 +1,4 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
|
3
2
|
import typer
|
4
3
|
from pydantic import BaseModel, field_validator
|
5
4
|
from rich.console import Console
|
@@ -129,15 +128,10 @@ cli_options = {
|
|
129
128
|
case_sensitive=False,
|
130
129
|
),
|
131
130
|
"create_pr": typer.Option(
|
132
|
-
False,
|
133
|
-
"-r",
|
134
|
-
"--pr",
|
135
|
-
help="Create a pull request to the upstream repository.",
|
131
|
+
False, "-r", "--pr", help="Create a pull request to the upstream repository."
|
136
132
|
),
|
137
133
|
"rich_ui": typer.Option(
|
138
|
-
False,
|
139
|
-
"--rich-ui",
|
140
|
-
help="Use the interactive Rich UI for a better experience.",
|
134
|
+
False, "--rich-ui", help="Use the interactive Rich UI for a better experience."
|
141
135
|
),
|
142
136
|
"ai_agent": typer.Option(
|
143
137
|
False,
|
@@ -195,12 +189,10 @@ def main(
|
|
195
189
|
create_pr=create_pr,
|
196
190
|
rich_ui=rich_ui,
|
197
191
|
)
|
198
|
-
|
199
192
|
if ai_agent:
|
200
193
|
import os
|
201
194
|
|
202
195
|
os.environ["AI_AGENT"] = "1"
|
203
|
-
|
204
196
|
if rich_ui:
|
205
197
|
from crackerjack.interactive import launch_interactive_cli
|
206
198
|
|
@@ -210,7 +202,6 @@ def main(
|
|
210
202
|
pkg_version = version("crackerjack")
|
211
203
|
except (ImportError, ModuleNotFoundError):
|
212
204
|
pkg_version = "0.19.8"
|
213
|
-
|
214
205
|
launch_interactive_cli(pkg_version)
|
215
206
|
else:
|
216
207
|
runner = create_crackerjack_runner(console=console)
|
@@ -0,0 +1,614 @@
|
|
1
|
+
import ast
|
2
|
+
import re
|
3
|
+
import subprocess
|
4
|
+
import typing as t
|
5
|
+
from contextlib import suppress
|
6
|
+
from pathlib import Path
|
7
|
+
from subprocess import CompletedProcess
|
8
|
+
from subprocess import run as execute
|
9
|
+
from tomllib import loads
|
10
|
+
from pydantic import BaseModel
|
11
|
+
from rich.console import Console
|
12
|
+
from tomli_w import dumps
|
13
|
+
from crackerjack.errors import ErrorCode, ExecutionError
|
14
|
+
|
15
|
+
config_files = (".gitignore", ".pre-commit-config.yaml", ".libcst.codemod.yaml")
|
16
|
+
interactive_hooks = ("refurb", "bandit", "pyright")
|
17
|
+
default_python_version = "3.13"
|
18
|
+
|
19
|
+
|
20
|
+
@t.runtime_checkable
|
21
|
+
class CommandRunner(t.Protocol):
|
22
|
+
def execute_command(
|
23
|
+
self, cmd: list[str], **kwargs: t.Any
|
24
|
+
) -> subprocess.CompletedProcess[str]: ...
|
25
|
+
|
26
|
+
|
27
|
+
@t.runtime_checkable
|
28
|
+
class OptionsProtocol(t.Protocol):
|
29
|
+
commit: bool
|
30
|
+
interactive: bool
|
31
|
+
doc: bool
|
32
|
+
no_config_updates: bool
|
33
|
+
verbose: bool
|
34
|
+
update_precommit: bool
|
35
|
+
clean: bool
|
36
|
+
test: bool
|
37
|
+
benchmark: bool
|
38
|
+
benchmark_regression: bool
|
39
|
+
benchmark_regression_threshold: float
|
40
|
+
test_workers: int = 0
|
41
|
+
test_timeout: int = 0
|
42
|
+
publish: t.Any | None
|
43
|
+
bump: t.Any | None
|
44
|
+
all: t.Any | None
|
45
|
+
ai_agent: bool = False
|
46
|
+
create_pr: bool = False
|
47
|
+
skip_hooks: bool = False
|
48
|
+
|
49
|
+
|
50
|
+
class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
51
|
+
console: Console
|
52
|
+
|
53
|
+
def clean_files(self, pkg_dir: Path | None) -> None:
|
54
|
+
if pkg_dir is None:
|
55
|
+
return
|
56
|
+
for file_path in pkg_dir.rglob("*.py"):
|
57
|
+
if not str(file_path.parent).startswith("__"):
|
58
|
+
self.clean_file(file_path)
|
59
|
+
with suppress(PermissionError, OSError):
|
60
|
+
pycache_dir = pkg_dir / "__pycache__"
|
61
|
+
if pycache_dir.exists():
|
62
|
+
for cache_file in pycache_dir.iterdir():
|
63
|
+
with suppress(PermissionError, OSError):
|
64
|
+
cache_file.unlink()
|
65
|
+
pycache_dir.rmdir()
|
66
|
+
parent_pycache = pkg_dir.parent / "__pycache__"
|
67
|
+
if parent_pycache.exists():
|
68
|
+
for cache_file in parent_pycache.iterdir():
|
69
|
+
with suppress(PermissionError, OSError):
|
70
|
+
cache_file.unlink()
|
71
|
+
parent_pycache.rmdir()
|
72
|
+
|
73
|
+
def clean_file(self, file_path: Path) -> None:
|
74
|
+
try:
|
75
|
+
code = file_path.read_text()
|
76
|
+
code = self.remove_docstrings(code)
|
77
|
+
code = self.remove_line_comments(code)
|
78
|
+
code = self.remove_extra_whitespace(code)
|
79
|
+
code = self.reformat_code(code)
|
80
|
+
file_path.write_text(code)
|
81
|
+
print(f"Cleaned: {file_path}")
|
82
|
+
except Exception as e:
|
83
|
+
print(f"Error cleaning {file_path}: {e}")
|
84
|
+
|
85
|
+
def remove_docstrings(self, code: str) -> str:
|
86
|
+
tree = ast.parse(code)
|
87
|
+
for node in ast.walk(tree):
|
88
|
+
if isinstance(
|
89
|
+
node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Module
|
90
|
+
):
|
91
|
+
if ast.get_docstring(node):
|
92
|
+
node.body = (
|
93
|
+
node.body[1:]
|
94
|
+
if isinstance(node.body[0], ast.Expr)
|
95
|
+
else node.body
|
96
|
+
)
|
97
|
+
return ast.unparse(tree)
|
98
|
+
|
99
|
+
def remove_line_comments(self, code: str) -> str:
|
100
|
+
lines = code.split("\n")
|
101
|
+
cleaned_lines = []
|
102
|
+
for line in lines:
|
103
|
+
if not line.strip():
|
104
|
+
cleaned_lines.append(line)
|
105
|
+
continue
|
106
|
+
in_string = None
|
107
|
+
result = []
|
108
|
+
i = 0
|
109
|
+
n = len(line)
|
110
|
+
while i < n:
|
111
|
+
char = line[i]
|
112
|
+
if char in ("'", '"') and (i == 0 or line[i - 1] != "\\"):
|
113
|
+
if in_string is None:
|
114
|
+
in_string = char
|
115
|
+
elif in_string == char:
|
116
|
+
in_string = None
|
117
|
+
result.append(char)
|
118
|
+
i += 1
|
119
|
+
elif char == "#" and in_string is None:
|
120
|
+
comment = line[i:].strip()
|
121
|
+
if re.match("^#\\s*(?:type:\\s*ignore|noqa)\\b", comment):
|
122
|
+
result.append(line[i:])
|
123
|
+
break
|
124
|
+
break
|
125
|
+
else:
|
126
|
+
result.append(char)
|
127
|
+
i += 1
|
128
|
+
cleaned_line = "".join(result).rstrip()
|
129
|
+
if cleaned_line or not line.strip():
|
130
|
+
cleaned_lines.append(cleaned_line or line)
|
131
|
+
return "\n".join(cleaned_lines)
|
132
|
+
|
133
|
+
def remove_extra_whitespace(self, code: str) -> str:
|
134
|
+
lines = code.split("\n")
|
135
|
+
cleaned_lines = []
|
136
|
+
for i, line in enumerate(lines):
|
137
|
+
line = line.rstrip()
|
138
|
+
if i > 0 and (not line) and (not cleaned_lines[-1]):
|
139
|
+
continue
|
140
|
+
cleaned_lines.append(line)
|
141
|
+
return "\n".join(cleaned_lines)
|
142
|
+
|
143
|
+
def reformat_code(self, code: str) -> str:
|
144
|
+
from crackerjack.errors import handle_error
|
145
|
+
|
146
|
+
try:
|
147
|
+
import tempfile
|
148
|
+
|
149
|
+
with tempfile.NamedTemporaryFile(
|
150
|
+
suffix=".py", mode="w+", delete=False
|
151
|
+
) as temp:
|
152
|
+
temp_path = Path(temp.name)
|
153
|
+
temp_path.write_text(code)
|
154
|
+
try:
|
155
|
+
result = subprocess.run(
|
156
|
+
["ruff", "format", str(temp_path)],
|
157
|
+
check=False,
|
158
|
+
capture_output=True,
|
159
|
+
text=True,
|
160
|
+
)
|
161
|
+
if result.returncode == 0:
|
162
|
+
formatted_code = temp_path.read_text()
|
163
|
+
else:
|
164
|
+
self.console.print(
|
165
|
+
f"[yellow]Ruff formatting failed: {result.stderr}[/yellow]"
|
166
|
+
)
|
167
|
+
handle_error(
|
168
|
+
ExecutionError(
|
169
|
+
message="Code formatting failed",
|
170
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
171
|
+
details=result.stderr,
|
172
|
+
recovery="Check Ruff configuration and formatting rules",
|
173
|
+
),
|
174
|
+
console=self.console,
|
175
|
+
)
|
176
|
+
formatted_code = code
|
177
|
+
except Exception as e:
|
178
|
+
self.console.print(f"[red]Error running Ruff: {e}[/red]")
|
179
|
+
handle_error(
|
180
|
+
ExecutionError(
|
181
|
+
message="Error running Ruff",
|
182
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
183
|
+
details=str(e),
|
184
|
+
recovery="Verify Ruff is installed and configured correctly",
|
185
|
+
),
|
186
|
+
console=self.console,
|
187
|
+
)
|
188
|
+
formatted_code = code
|
189
|
+
finally:
|
190
|
+
with suppress(FileNotFoundError):
|
191
|
+
temp_path.unlink()
|
192
|
+
return formatted_code
|
193
|
+
except Exception as e:
|
194
|
+
self.console.print(f"[red]Error during reformatting: {e}[/red]")
|
195
|
+
handle_error(
|
196
|
+
ExecutionError(
|
197
|
+
message="Error during reformatting",
|
198
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
199
|
+
details=str(e),
|
200
|
+
recovery="Check file permissions and disk space",
|
201
|
+
),
|
202
|
+
console=self.console,
|
203
|
+
)
|
204
|
+
return code
|
205
|
+
|
206
|
+
|
207
|
+
class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
208
|
+
our_path: Path
|
209
|
+
pkg_path: Path
|
210
|
+
pkg_name: str
|
211
|
+
console: Console
|
212
|
+
our_toml_path: Path | None = None
|
213
|
+
pkg_toml_path: Path | None = None
|
214
|
+
python_version: str = default_python_version
|
215
|
+
dry_run: bool = False
|
216
|
+
|
217
|
+
def swap_package_name(self, value: list[str] | str) -> list[str] | str:
|
218
|
+
if isinstance(value, list):
|
219
|
+
value.remove("crackerjack")
|
220
|
+
value.append(self.pkg_name)
|
221
|
+
else:
|
222
|
+
value = value.replace("crackerjack", self.pkg_name)
|
223
|
+
return value
|
224
|
+
|
225
|
+
def update_pyproject_configs(self) -> None:
|
226
|
+
self._setup_toml_paths()
|
227
|
+
if self._is_crackerjack_project():
|
228
|
+
self._handle_crackerjack_project()
|
229
|
+
return
|
230
|
+
our_toml_config = self._load_our_toml()
|
231
|
+
pkg_toml_config = self._load_pkg_toml()
|
232
|
+
self._ensure_required_sections(pkg_toml_config)
|
233
|
+
self._update_tool_settings(our_toml_config, pkg_toml_config)
|
234
|
+
self._update_python_version(our_toml_config, pkg_toml_config)
|
235
|
+
self._save_pkg_toml(pkg_toml_config)
|
236
|
+
|
237
|
+
def _setup_toml_paths(self) -> None:
|
238
|
+
toml_file = "pyproject.toml"
|
239
|
+
self.our_toml_path = self.our_path / toml_file
|
240
|
+
self.pkg_toml_path = self.pkg_path / toml_file
|
241
|
+
|
242
|
+
def _is_crackerjack_project(self) -> bool:
|
243
|
+
return self.pkg_path.stem == "crackerjack"
|
244
|
+
|
245
|
+
def _handle_crackerjack_project(self) -> None:
|
246
|
+
if self.our_toml_path and self.pkg_toml_path:
|
247
|
+
self.our_toml_path.write_text(self.pkg_toml_path.read_text())
|
248
|
+
|
249
|
+
def _load_our_toml(self) -> dict[str, t.Any]:
|
250
|
+
if self.our_toml_path:
|
251
|
+
return loads(self.our_toml_path.read_text())
|
252
|
+
return {}
|
253
|
+
|
254
|
+
def _load_pkg_toml(self) -> dict[str, t.Any]:
|
255
|
+
if self.pkg_toml_path:
|
256
|
+
return loads(self.pkg_toml_path.read_text())
|
257
|
+
return {}
|
258
|
+
|
259
|
+
def _ensure_required_sections(self, pkg_toml_config: dict[str, t.Any]) -> None:
|
260
|
+
pkg_toml_config.setdefault("tool", {})
|
261
|
+
pkg_toml_config.setdefault("project", {})
|
262
|
+
|
263
|
+
def _update_tool_settings(
|
264
|
+
self, our_toml_config: dict[str, t.Any], pkg_toml_config: dict[str, t.Any]
|
265
|
+
) -> None:
|
266
|
+
for tool, settings in our_toml_config.get("tool", {}).items():
|
267
|
+
for setting, value in settings.items():
|
268
|
+
if isinstance(value, dict):
|
269
|
+
for k, v in {
|
270
|
+
x: self.swap_package_name(y)
|
271
|
+
for x, y in value.items()
|
272
|
+
if isinstance(y, str | list) and "crackerjack" in str(y)
|
273
|
+
}.items():
|
274
|
+
settings[setting][k] = v
|
275
|
+
elif isinstance(value, str | list) and "crackerjack" in str(value):
|
276
|
+
value = self.swap_package_name(value)
|
277
|
+
settings[setting] = value
|
278
|
+
if setting in (
|
279
|
+
"exclude-deps",
|
280
|
+
"exclude",
|
281
|
+
"excluded",
|
282
|
+
"skips",
|
283
|
+
"ignore",
|
284
|
+
) and isinstance(value, list):
|
285
|
+
conf = pkg_toml_config["tool"].get(tool, {}).get(setting, [])
|
286
|
+
settings[setting] = list(set(conf + value))
|
287
|
+
pkg_toml_config["tool"][tool] = settings
|
288
|
+
|
289
|
+
def _update_python_version(
|
290
|
+
self, our_toml_config: dict[str, t.Any], pkg_toml_config: dict[str, t.Any]
|
291
|
+
) -> None:
|
292
|
+
python_version_pattern = "\\s*W*(\\d\\.\\d*)"
|
293
|
+
requires_python = our_toml_config.get("project", {}).get("requires-python", "")
|
294
|
+
classifiers = []
|
295
|
+
for classifier in pkg_toml_config.get("project", {}).get("classifiers", []):
|
296
|
+
classifier = re.sub(
|
297
|
+
python_version_pattern, f" {self.python_version}", classifier
|
298
|
+
)
|
299
|
+
classifiers.append(classifier)
|
300
|
+
pkg_toml_config["project"]["classifiers"] = classifiers
|
301
|
+
if requires_python:
|
302
|
+
pkg_toml_config["project"]["requires-python"] = requires_python
|
303
|
+
|
304
|
+
def _save_pkg_toml(self, pkg_toml_config: dict[str, t.Any]) -> None:
|
305
|
+
if self.pkg_toml_path:
|
306
|
+
self.pkg_toml_path.write_text(dumps(pkg_toml_config))
|
307
|
+
|
308
|
+
def copy_configs(self) -> None:
|
309
|
+
for config in config_files:
|
310
|
+
config_path = self.our_path / config
|
311
|
+
pkg_config_path = self.pkg_path / config
|
312
|
+
pkg_config_path.touch()
|
313
|
+
if self.pkg_path.stem == "crackerjack":
|
314
|
+
config_path.write_text(pkg_config_path.read_text())
|
315
|
+
continue
|
316
|
+
if config != ".gitignore":
|
317
|
+
pkg_config_path.write_text(
|
318
|
+
config_path.read_text().replace("crackerjack", self.pkg_name)
|
319
|
+
)
|
320
|
+
self.execute_command(["git", "add", config])
|
321
|
+
|
322
|
+
def execute_command(
|
323
|
+
self, cmd: list[str], **kwargs: t.Any
|
324
|
+
) -> subprocess.CompletedProcess[str]:
|
325
|
+
if self.dry_run:
|
326
|
+
self.console.print(f"[yellow]Would run: {' '.join(cmd)}[/yellow]")
|
327
|
+
return CompletedProcess(cmd, 0, "", "")
|
328
|
+
return execute(cmd, **kwargs)
|
329
|
+
|
330
|
+
|
331
|
+
class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
332
|
+
our_path: Path
|
333
|
+
pkg_path: Path
|
334
|
+
pkg_dir: Path | None = None
|
335
|
+
pkg_name: str = "crackerjack"
|
336
|
+
console: Console
|
337
|
+
code_cleaner: CodeCleaner
|
338
|
+
config_manager: ConfigManager
|
339
|
+
dry_run: bool = False
|
340
|
+
|
341
|
+
def run_interactive(self, hook: str) -> None:
|
342
|
+
success: bool = False
|
343
|
+
while not success:
|
344
|
+
fail = self.execute_command(
|
345
|
+
["pre-commit", "run", hook.lower(), "--all-files"]
|
346
|
+
)
|
347
|
+
if fail.returncode > 0:
|
348
|
+
retry = input(f"\n\n{hook.title()} failed. Retry? (y/N): ")
|
349
|
+
self.console.print()
|
350
|
+
if retry.strip().lower() == "y":
|
351
|
+
continue
|
352
|
+
raise SystemExit(1)
|
353
|
+
success = True
|
354
|
+
|
355
|
+
def update_pkg_configs(self) -> None:
|
356
|
+
self.config_manager.copy_configs()
|
357
|
+
installed_pkgs = self.execute_command(
|
358
|
+
["pdm", "list", "--freeze"], capture_output=True, text=True
|
359
|
+
).stdout.splitlines()
|
360
|
+
if not len([pkg for pkg in installed_pkgs if "pre-commit" in pkg]):
|
361
|
+
self.console.print("Initializing project...")
|
362
|
+
self.execute_command(["pdm", "self", "add", "keyring"])
|
363
|
+
self.execute_command(["pdm", "config", "python.use_uv", "true"])
|
364
|
+
self.execute_command(["git", "init"])
|
365
|
+
self.execute_command(["git", "branch", "-m", "main"])
|
366
|
+
self.execute_command(["git", "add", "pyproject.toml"])
|
367
|
+
self.execute_command(["git", "add", "pdm.lock"])
|
368
|
+
self.execute_command(["pre-commit", "install"])
|
369
|
+
self.execute_command(["git", "config", "advice.addIgnoredFile", "false"])
|
370
|
+
self.config_manager.update_pyproject_configs()
|
371
|
+
|
372
|
+
def run_pre_commit(self) -> None:
|
373
|
+
self.console.print("\nRunning pre-commit hooks...\n")
|
374
|
+
check_all = self.execute_command(["pre-commit", "run", "--all-files"])
|
375
|
+
if check_all.returncode > 0:
|
376
|
+
check_all = self.execute_command(["pre-commit", "run", "--all-files"])
|
377
|
+
if check_all.returncode > 0:
|
378
|
+
self.console.print("\n\nPre-commit failed. Please fix errors.\n")
|
379
|
+
raise SystemExit(1)
|
380
|
+
|
381
|
+
def execute_command(
|
382
|
+
self, cmd: list[str], **kwargs: t.Any
|
383
|
+
) -> subprocess.CompletedProcess[str]:
|
384
|
+
if self.dry_run:
|
385
|
+
self.console.print(f"[yellow]Would run: {' '.join(cmd)}[/yellow]")
|
386
|
+
return CompletedProcess(cmd, 0, "", "")
|
387
|
+
return execute(cmd, **kwargs)
|
388
|
+
|
389
|
+
|
390
|
+
class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
391
|
+
our_path: Path = Path(__file__).parent
|
392
|
+
pkg_path: Path = Path(Path.cwd())
|
393
|
+
pkg_dir: Path | None = None
|
394
|
+
pkg_name: str = "crackerjack"
|
395
|
+
python_version: str = default_python_version
|
396
|
+
console: Console = Console(force_terminal=True)
|
397
|
+
dry_run: bool = False
|
398
|
+
code_cleaner: CodeCleaner | None = None
|
399
|
+
config_manager: ConfigManager | None = None
|
400
|
+
project_manager: ProjectManager | None = None
|
401
|
+
|
402
|
+
def __init__(self, **data: t.Any) -> None:
|
403
|
+
super().__init__(**data)
|
404
|
+
self.code_cleaner = CodeCleaner(console=self.console)
|
405
|
+
self.config_manager = ConfigManager(
|
406
|
+
our_path=self.our_path,
|
407
|
+
pkg_path=self.pkg_path,
|
408
|
+
pkg_name=self.pkg_name,
|
409
|
+
console=self.console,
|
410
|
+
python_version=self.python_version,
|
411
|
+
dry_run=self.dry_run,
|
412
|
+
)
|
413
|
+
self.project_manager = ProjectManager(
|
414
|
+
our_path=self.our_path,
|
415
|
+
pkg_path=self.pkg_path,
|
416
|
+
pkg_dir=self.pkg_dir,
|
417
|
+
pkg_name=self.pkg_name,
|
418
|
+
console=self.console,
|
419
|
+
code_cleaner=self.code_cleaner,
|
420
|
+
config_manager=self.config_manager,
|
421
|
+
dry_run=self.dry_run,
|
422
|
+
)
|
423
|
+
|
424
|
+
def _setup_package(self) -> None:
|
425
|
+
self.pkg_name = self.pkg_path.stem.lower().replace("-", "_")
|
426
|
+
self.pkg_dir = self.pkg_path / self.pkg_name
|
427
|
+
self.pkg_dir.mkdir(exist_ok=True)
|
428
|
+
self.console.print("\nCrackerjacking...\n")
|
429
|
+
self.config_manager.pkg_name = self.pkg_name
|
430
|
+
self.project_manager.pkg_name = self.pkg_name
|
431
|
+
self.project_manager.pkg_dir = self.pkg_dir
|
432
|
+
|
433
|
+
def _update_project(self, options: t.Any) -> None:
|
434
|
+
if not options.no_config_updates:
|
435
|
+
self.project_manager.update_pkg_configs()
|
436
|
+
result: CompletedProcess[str] = self.execute_command(
|
437
|
+
["pdm", "install"], capture_output=True, text=True
|
438
|
+
)
|
439
|
+
if result.returncode == 0:
|
440
|
+
self.console.print("PDM installed: ✅\n")
|
441
|
+
else:
|
442
|
+
self.console.print(
|
443
|
+
"\n\n❌ PDM installation failed. Is PDM is installed? Run `pipx install pdm` and try again.\n\n"
|
444
|
+
)
|
445
|
+
|
446
|
+
def _update_precommit(self, options: t.Any) -> None:
|
447
|
+
if self.pkg_path.stem == "crackerjack" and options.update_precommit:
|
448
|
+
self.execute_command(["pre-commit", "autoupdate"])
|
449
|
+
|
450
|
+
def _run_interactive_hooks(self, options: t.Any) -> None:
|
451
|
+
if options.interactive:
|
452
|
+
for hook in interactive_hooks:
|
453
|
+
self.project_manager.run_interactive(hook)
|
454
|
+
|
455
|
+
def _clean_project(self, options: t.Any) -> None:
|
456
|
+
if options.clean:
|
457
|
+
if self.pkg_dir:
|
458
|
+
self.code_cleaner.clean_files(self.pkg_dir)
|
459
|
+
if self.pkg_path.stem == "crackerjack":
|
460
|
+
tests_dir = self.pkg_path / "tests"
|
461
|
+
if tests_dir.exists() and tests_dir.is_dir():
|
462
|
+
self.console.print("\nCleaning tests directory...\n")
|
463
|
+
self.code_cleaner.clean_files(tests_dir)
|
464
|
+
|
465
|
+
def _prepare_pytest_command(self, options: OptionsProtocol) -> list[str]:
|
466
|
+
test = ["pytest"]
|
467
|
+
project_size = self._detect_project_size()
|
468
|
+
if options.test_timeout > 0:
|
469
|
+
test_timeout = options.test_timeout
|
470
|
+
else:
|
471
|
+
test_timeout = (
|
472
|
+
300
|
473
|
+
if project_size == "large"
|
474
|
+
else 120
|
475
|
+
if project_size == "medium"
|
476
|
+
else 60
|
477
|
+
)
|
478
|
+
test.extend(
|
479
|
+
[
|
480
|
+
"--capture=fd",
|
481
|
+
"--tb=short",
|
482
|
+
"--no-header",
|
483
|
+
"--disable-warnings",
|
484
|
+
"--durations=0",
|
485
|
+
f"--timeout={test_timeout}",
|
486
|
+
]
|
487
|
+
)
|
488
|
+
if options.benchmark or options.benchmark_regression:
|
489
|
+
if options.benchmark:
|
490
|
+
test.append("--benchmark")
|
491
|
+
if options.benchmark_regression:
|
492
|
+
test.extend(
|
493
|
+
[
|
494
|
+
"--benchmark-regression",
|
495
|
+
f"--benchmark-regression-threshold={options.benchmark_regression_threshold}",
|
496
|
+
]
|
497
|
+
)
|
498
|
+
elif options.test_workers > 0:
|
499
|
+
if options.test_workers == 1:
|
500
|
+
test.append("-vs")
|
501
|
+
else:
|
502
|
+
test.extend(["-xvs", "-n", str(options.test_workers)])
|
503
|
+
elif project_size == "large":
|
504
|
+
test.extend(["-xvs", "-n", "2"])
|
505
|
+
elif project_size == "medium":
|
506
|
+
test.extend(["-xvs", "-n", "auto"])
|
507
|
+
else:
|
508
|
+
test.append("-xvs")
|
509
|
+
return test
|
510
|
+
|
511
|
+
def _detect_project_size(self) -> str:
|
512
|
+
if self.pkg_name in ("acb", "fastblocks"):
|
513
|
+
return "large"
|
514
|
+
try:
|
515
|
+
py_files = list(self.pkg_path.rglob("*.py"))
|
516
|
+
test_files = list(self.pkg_path.rglob("test_*.py"))
|
517
|
+
total_files = len(py_files)
|
518
|
+
num_test_files = len(test_files)
|
519
|
+
if total_files > 100 or num_test_files > 50:
|
520
|
+
return "large"
|
521
|
+
elif total_files > 50 or num_test_files > 20:
|
522
|
+
return "medium"
|
523
|
+
else:
|
524
|
+
return "small"
|
525
|
+
except Exception:
|
526
|
+
return "medium"
|
527
|
+
|
528
|
+
def _run_tests(self, options: t.Any) -> None:
|
529
|
+
if options.test:
|
530
|
+
self.console.print("\n\nRunning tests...\n")
|
531
|
+
test_command = self._prepare_pytest_command(options)
|
532
|
+
result = self.execute_command(test_command, capture_output=True, text=True)
|
533
|
+
if result.stdout:
|
534
|
+
self.console.print(result.stdout)
|
535
|
+
if result.returncode > 0:
|
536
|
+
if result.stderr:
|
537
|
+
self.console.print(result.stderr)
|
538
|
+
self.console.print("\n\n❌ Tests failed. Please fix errors.\n")
|
539
|
+
return
|
540
|
+
self.console.print("\n\n✅ Tests passed successfully!\n")
|
541
|
+
|
542
|
+
def _bump_version(self, options: OptionsProtocol) -> None:
|
543
|
+
for option in (options.publish, options.bump):
|
544
|
+
if option:
|
545
|
+
self.execute_command(["pdm", "bump", option])
|
546
|
+
break
|
547
|
+
|
548
|
+
def _publish_project(self, options: OptionsProtocol) -> None:
|
549
|
+
if options.publish:
|
550
|
+
build = self.execute_command(
|
551
|
+
["pdm", "build"], capture_output=True, text=True
|
552
|
+
)
|
553
|
+
self.console.print(build.stdout)
|
554
|
+
if build.returncode > 0:
|
555
|
+
self.console.print(build.stderr)
|
556
|
+
self.console.print("\n\nBuild failed. Please fix errors.\n")
|
557
|
+
raise SystemExit(1)
|
558
|
+
self.execute_command(["pdm", "publish", "--no-build"])
|
559
|
+
|
560
|
+
def _commit_and_push(self, options: OptionsProtocol) -> None:
|
561
|
+
if options.commit:
|
562
|
+
commit_msg = input("\nCommit message: ")
|
563
|
+
self.execute_command(
|
564
|
+
["git", "commit", "-m", commit_msg, "--no-verify", "--", "."]
|
565
|
+
)
|
566
|
+
self.execute_command(["git", "push", "origin", "main"])
|
567
|
+
|
568
|
+
def execute_command(
|
569
|
+
self, cmd: list[str], **kwargs: t.Any
|
570
|
+
) -> subprocess.CompletedProcess[str]:
|
571
|
+
if self.dry_run:
|
572
|
+
self.console.print(f"[yellow]Would run: {' '.join(cmd)}[/yellow]")
|
573
|
+
return CompletedProcess(cmd, 0, "", "")
|
574
|
+
return execute(cmd, **kwargs)
|
575
|
+
|
576
|
+
def process(self, options: OptionsProtocol) -> None:
|
577
|
+
if options.all:
|
578
|
+
options.clean = True
|
579
|
+
options.test = True
|
580
|
+
options.publish = options.all
|
581
|
+
options.commit = True
|
582
|
+
self._setup_package()
|
583
|
+
self._update_project(options)
|
584
|
+
self._update_precommit(options)
|
585
|
+
self._run_interactive_hooks(options)
|
586
|
+
self._clean_project(options)
|
587
|
+
if not options.skip_hooks:
|
588
|
+
self.project_manager.run_pre_commit()
|
589
|
+
else:
|
590
|
+
self.console.print("Skipping pre-commit hooks")
|
591
|
+
self._run_tests(options)
|
592
|
+
self._bump_version(options)
|
593
|
+
self._publish_project(options)
|
594
|
+
self._commit_and_push(options)
|
595
|
+
self.console.print("\n🍺 Crackerjack complete!\n")
|
596
|
+
|
597
|
+
|
598
|
+
crackerjack_it = Crackerjack().process
|
599
|
+
|
600
|
+
|
601
|
+
def create_crackerjack_runner(
|
602
|
+
console: Console | None = None,
|
603
|
+
our_path: Path | None = None,
|
604
|
+
pkg_path: Path | None = None,
|
605
|
+
python_version: str = default_python_version,
|
606
|
+
dry_run: bool = False,
|
607
|
+
) -> Crackerjack:
|
608
|
+
return Crackerjack(
|
609
|
+
console=console or Console(force_terminal=True),
|
610
|
+
our_path=our_path or Path(__file__).parent,
|
611
|
+
pkg_path=pkg_path or Path.cwd(),
|
612
|
+
python_version=python_version,
|
613
|
+
dry_run=dry_run,
|
614
|
+
)
|