crackerjack 0.20.19__py3-none-any.whl → 0.21.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crackerjack/.pre-commit-config.yaml +10 -4
- crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
- crackerjack/.ruff_cache/0.12.0/5056746222905752453 +0 -0
- crackerjack/.ruff_cache/0.12.1/5056746222905752453 +0 -0
- crackerjack/__main__.py +5 -8
- crackerjack/crackerjack.py +241 -121
- crackerjack/interactive.py +119 -66
- crackerjack/pyproject.toml +9 -6
- {crackerjack-0.20.19.dist-info → crackerjack-0.21.0.dist-info}/METADATA +6 -6
- {crackerjack-0.20.19.dist-info → crackerjack-0.21.0.dist-info}/RECORD +13 -11
- {crackerjack-0.20.19.dist-info → crackerjack-0.21.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.20.19.dist-info → crackerjack-0.21.0.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.20.19.dist-info → crackerjack-0.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -26,7 +26,7 @@ repos:
|
|
26
26
|
|
27
27
|
# Package management - once structure is valid
|
28
28
|
- repo: https://github.com/pdm-project/pdm
|
29
|
-
rev: 2.
|
29
|
+
rev: 2.25.3
|
30
30
|
hooks:
|
31
31
|
- id: pdm-lock-check
|
32
32
|
- id: pdm-sync
|
@@ -34,7 +34,7 @@ repos:
|
|
34
34
|
- keyring
|
35
35
|
|
36
36
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
37
|
-
rev: 0.7.
|
37
|
+
rev: 0.7.15
|
38
38
|
hooks:
|
39
39
|
- id: uv-lock
|
40
40
|
files: ^pyproject\.toml$
|
@@ -55,7 +55,7 @@ repos:
|
|
55
55
|
- tomli
|
56
56
|
|
57
57
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
58
|
-
rev: v0.
|
58
|
+
rev: v0.12.1
|
59
59
|
hooks:
|
60
60
|
- id: ruff-check
|
61
61
|
- id: ruff-format
|
@@ -71,6 +71,12 @@ repos:
|
|
71
71
|
hooks:
|
72
72
|
- id: creosote
|
73
73
|
|
74
|
+
- repo: https://github.com/rohaquinlop/complexipy-pre-commit
|
75
|
+
rev: v3.0.0
|
76
|
+
hooks:
|
77
|
+
- id: complexipy
|
78
|
+
args: ["-d", "low"]
|
79
|
+
|
74
80
|
- repo: https://github.com/dosisod/refurb
|
75
81
|
rev: v2.1.0
|
76
82
|
hooks:
|
@@ -95,7 +101,7 @@ repos:
|
|
95
101
|
- libcst>=1.1.0
|
96
102
|
|
97
103
|
- repo: https://github.com/PyCQA/bandit
|
98
|
-
rev: '1.8.
|
104
|
+
rev: '1.8.5'
|
99
105
|
hooks:
|
100
106
|
- id: bandit
|
101
107
|
args: ["-c", "pyproject.toml"]
|
Binary file
|
Binary file
|
Binary file
|
crackerjack/__main__.py
CHANGED
@@ -40,7 +40,6 @@ class Options(BaseModel):
|
|
40
40
|
ai_agent: bool = False
|
41
41
|
create_pr: bool = False
|
42
42
|
skip_hooks: bool = False
|
43
|
-
rich_ui: bool = False
|
44
43
|
|
45
44
|
@classmethod
|
46
45
|
@field_validator("publish", "bump", mode="before")
|
@@ -59,7 +58,10 @@ class Options(BaseModel):
|
|
59
58
|
cli_options = {
|
60
59
|
"commit": typer.Option(False, "-c", "--commit", help="Commit changes to Git."),
|
61
60
|
"interactive": typer.Option(
|
62
|
-
False,
|
61
|
+
False,
|
62
|
+
"-i",
|
63
|
+
"--interactive",
|
64
|
+
help="Use the interactive Rich UI for a better experience.",
|
63
65
|
),
|
64
66
|
"doc": typer.Option(False, "-d", "--doc", help="Generate documentation."),
|
65
67
|
"no_config_updates": typer.Option(
|
@@ -131,9 +133,6 @@ cli_options = {
|
|
131
133
|
"create_pr": typer.Option(
|
132
134
|
False, "-r", "--pr", help="Create a pull request to the upstream repository."
|
133
135
|
),
|
134
|
-
"rich_ui": typer.Option(
|
135
|
-
False, "--rich-ui", help="Use the interactive Rich UI for a better experience."
|
136
|
-
),
|
137
136
|
"ai_agent": typer.Option(
|
138
137
|
False,
|
139
138
|
"--ai-agent",
|
@@ -165,7 +164,6 @@ def main(
|
|
165
164
|
test_timeout: int = cli_options["test_timeout"],
|
166
165
|
skip_hooks: bool = cli_options["skip_hooks"],
|
167
166
|
create_pr: bool = cli_options["create_pr"],
|
168
|
-
rich_ui: bool = cli_options["rich_ui"],
|
169
167
|
ai_agent: bool = cli_options["ai_agent"],
|
170
168
|
) -> None:
|
171
169
|
options = Options(
|
@@ -188,13 +186,12 @@ def main(
|
|
188
186
|
all=all,
|
189
187
|
ai_agent=ai_agent,
|
190
188
|
create_pr=create_pr,
|
191
|
-
rich_ui=rich_ui,
|
192
189
|
)
|
193
190
|
if ai_agent:
|
194
191
|
import os
|
195
192
|
|
196
193
|
os.environ["AI_AGENT"] = "1"
|
197
|
-
if
|
194
|
+
if interactive:
|
198
195
|
from crackerjack.interactive import launch_interactive_cli
|
199
196
|
|
200
197
|
try:
|
crackerjack/crackerjack.py
CHANGED
@@ -13,7 +13,6 @@ from tomli_w import dumps
|
|
13
13
|
from crackerjack.errors import ErrorCode, ExecutionError
|
14
14
|
|
15
15
|
config_files = (".gitignore", ".pre-commit-config.yaml", ".libcst.codemod.yaml")
|
16
|
-
interactive_hooks = ("refurb", "bandit", "pyright")
|
17
16
|
default_python_version = "3.13"
|
18
17
|
|
19
18
|
|
@@ -85,36 +84,20 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
85
84
|
def remove_docstrings(self, code: str) -> str:
|
86
85
|
lines = code.split("\n")
|
87
86
|
cleaned_lines = []
|
88
|
-
|
89
|
-
docstring_delimiter = None
|
90
|
-
waiting_for_docstring = False
|
87
|
+
docstring_state = {"in_docstring": False, "delimiter": None, "waiting": False}
|
91
88
|
for line in lines:
|
92
89
|
stripped = line.strip()
|
93
|
-
if
|
94
|
-
|
90
|
+
if self._is_function_or_class_definition(stripped):
|
91
|
+
docstring_state["waiting"] = True
|
95
92
|
cleaned_lines.append(line)
|
96
93
|
continue
|
97
|
-
if
|
98
|
-
if
|
99
|
-
|
100
|
-
docstring_delimiter = stripped[:3]
|
101
|
-
else:
|
102
|
-
docstring_delimiter = stripped[0]
|
103
|
-
if stripped.endswith(docstring_delimiter) and len(stripped) > len(
|
104
|
-
docstring_delimiter
|
105
|
-
):
|
106
|
-
waiting_for_docstring = False
|
107
|
-
continue
|
108
|
-
else:
|
109
|
-
in_docstring = True
|
110
|
-
waiting_for_docstring = False
|
111
|
-
continue
|
94
|
+
if docstring_state["waiting"] and stripped:
|
95
|
+
if self._handle_docstring_start(stripped, docstring_state):
|
96
|
+
continue
|
112
97
|
else:
|
113
|
-
|
114
|
-
if in_docstring:
|
115
|
-
if
|
116
|
-
in_docstring = False
|
117
|
-
docstring_delimiter = None
|
98
|
+
docstring_state["waiting"] = False
|
99
|
+
if docstring_state["in_docstring"]:
|
100
|
+
if self._handle_docstring_end(stripped, docstring_state):
|
118
101
|
continue
|
119
102
|
else:
|
120
103
|
continue
|
@@ -122,6 +105,35 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
122
105
|
|
123
106
|
return "\n".join(cleaned_lines)
|
124
107
|
|
108
|
+
def _is_function_or_class_definition(self, stripped_line: str) -> bool:
|
109
|
+
return stripped_line.startswith(("def ", "class ", "async def "))
|
110
|
+
|
111
|
+
def _handle_docstring_start(self, stripped: str, state: dict[str, t.Any]) -> bool:
|
112
|
+
if not stripped.startswith(('"""', "'''", '"', "'")):
|
113
|
+
return False
|
114
|
+
if stripped.startswith(('"""', "'''")):
|
115
|
+
delimiter = stripped[:3]
|
116
|
+
else:
|
117
|
+
delimiter = stripped[0]
|
118
|
+
state["delimiter"] = delimiter
|
119
|
+
if self._is_single_line_docstring(stripped, delimiter):
|
120
|
+
state["waiting"] = False
|
121
|
+
return True
|
122
|
+
else:
|
123
|
+
state["in_docstring"] = True
|
124
|
+
state["waiting"] = False
|
125
|
+
return True
|
126
|
+
|
127
|
+
def _is_single_line_docstring(self, stripped: str, delimiter: str) -> bool:
|
128
|
+
return stripped.endswith(delimiter) and len(stripped) > len(delimiter)
|
129
|
+
|
130
|
+
def _handle_docstring_end(self, stripped: str, state: dict[str, t.Any]) -> bool:
|
131
|
+
if state["delimiter"] and stripped.endswith(state["delimiter"]):
|
132
|
+
state["in_docstring"] = False
|
133
|
+
state["delimiter"] = None
|
134
|
+
return True
|
135
|
+
return True
|
136
|
+
|
125
137
|
def remove_line_comments(self, code: str) -> str:
|
126
138
|
lines = code.split("\n")
|
127
139
|
cleaned_lines = []
|
@@ -129,87 +141,153 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
129
141
|
if not line.strip():
|
130
142
|
cleaned_lines.append(line)
|
131
143
|
continue
|
132
|
-
|
133
|
-
result = []
|
134
|
-
i = 0
|
135
|
-
n = len(line)
|
136
|
-
while i < n:
|
137
|
-
char = line[i]
|
138
|
-
if char in ("'", '"') and (i == 0 or line[i - 1] != "\\"):
|
139
|
-
if in_string is None:
|
140
|
-
in_string = char
|
141
|
-
elif in_string == char:
|
142
|
-
in_string = None
|
143
|
-
result.append(char)
|
144
|
-
i += 1
|
145
|
-
elif char == "#" and in_string is None:
|
146
|
-
comment = line[i:].strip()
|
147
|
-
if re.match(
|
148
|
-
r"^#\s*(?:type:\s*ignore(?:\[.*?\])?|noqa|nosec|pragma:\s*no\s*cover|pylint:\s*disable|mypy:\s*ignore)",
|
149
|
-
comment,
|
150
|
-
):
|
151
|
-
result.append(line[i:])
|
152
|
-
break
|
153
|
-
break
|
154
|
-
else:
|
155
|
-
result.append(char)
|
156
|
-
i += 1
|
157
|
-
cleaned_line = "".join(result).rstrip()
|
144
|
+
cleaned_line = self._process_line_for_comments(line)
|
158
145
|
if cleaned_line or not line.strip():
|
159
146
|
cleaned_lines.append(cleaned_line or line)
|
147
|
+
|
160
148
|
return "\n".join(cleaned_lines)
|
161
149
|
|
150
|
+
def _process_line_for_comments(self, line: str) -> str:
|
151
|
+
result = []
|
152
|
+
string_state = {"in_string": None}
|
153
|
+
for i, char in enumerate(line):
|
154
|
+
if self._handle_string_character(char, i, line, string_state, result):
|
155
|
+
continue
|
156
|
+
elif self._handle_comment_character(char, i, line, string_state, result):
|
157
|
+
break
|
158
|
+
else:
|
159
|
+
result.append(char)
|
160
|
+
|
161
|
+
return "".join(result).rstrip()
|
162
|
+
|
163
|
+
def _handle_string_character(
|
164
|
+
self,
|
165
|
+
char: str,
|
166
|
+
index: int,
|
167
|
+
line: str,
|
168
|
+
string_state: dict[str, t.Any],
|
169
|
+
result: list[str],
|
170
|
+
) -> bool:
|
171
|
+
"""Handle string quote characters. Returns True if character was handled."""
|
172
|
+
if char not in ("'", '"'):
|
173
|
+
return False
|
174
|
+
|
175
|
+
if index > 0 and line[index - 1] == "\\":
|
176
|
+
return False
|
177
|
+
|
178
|
+
if string_state["in_string"] is None:
|
179
|
+
string_state["in_string"] = char
|
180
|
+
elif string_state["in_string"] == char:
|
181
|
+
string_state["in_string"] = None
|
182
|
+
|
183
|
+
result.append(char)
|
184
|
+
return True
|
185
|
+
|
186
|
+
def _handle_comment_character(
|
187
|
+
self,
|
188
|
+
char: str,
|
189
|
+
index: int,
|
190
|
+
line: str,
|
191
|
+
string_state: dict[str, t.Any],
|
192
|
+
result: list[str],
|
193
|
+
) -> bool:
|
194
|
+
"""Handle comment character. Returns True if comment was found."""
|
195
|
+
if char != "#" or string_state["in_string"] is not None:
|
196
|
+
return False
|
197
|
+
|
198
|
+
comment = line[index:].strip()
|
199
|
+
if self._is_special_comment_line(comment):
|
200
|
+
result.append(line[index:])
|
201
|
+
|
202
|
+
return True
|
203
|
+
|
204
|
+
def _is_special_comment_line(self, comment: str) -> bool:
|
205
|
+
special_comment_pattern = (
|
206
|
+
r"^#\s*(?:type:\s*ignore(?:\[.*?\])?|noqa|nosec|pragma:\s*no\s*cover"
|
207
|
+
r"|pylint:\s*disable|mypy:\s*ignore)"
|
208
|
+
)
|
209
|
+
return bool(re.match(special_comment_pattern, comment))
|
210
|
+
|
162
211
|
def remove_extra_whitespace(self, code: str) -> str:
|
163
212
|
lines = code.split("\n")
|
164
213
|
cleaned_lines = []
|
165
|
-
|
166
|
-
function_indent = 0
|
214
|
+
function_tracker = {"in_function": False, "function_indent": 0}
|
167
215
|
for i, line in enumerate(lines):
|
168
216
|
line = line.rstrip()
|
169
217
|
stripped_line = line.lstrip()
|
170
|
-
|
171
|
-
in_function = True
|
172
|
-
function_indent = len(line) - len(stripped_line)
|
173
|
-
elif (
|
174
|
-
in_function
|
175
|
-
and line
|
176
|
-
and (len(line) - len(stripped_line) <= function_indent)
|
177
|
-
and (not stripped_line.startswith(("@", "#")))
|
178
|
-
):
|
179
|
-
in_function = False
|
180
|
-
function_indent = 0
|
218
|
+
self._update_function_state(line, stripped_line, function_tracker)
|
181
219
|
if not line:
|
182
|
-
if
|
220
|
+
if self._should_skip_empty_line(
|
221
|
+
i, lines, cleaned_lines, function_tracker
|
222
|
+
):
|
183
223
|
continue
|
184
|
-
if in_function:
|
185
|
-
next_line_idx = i + 1
|
186
|
-
if next_line_idx < len(lines):
|
187
|
-
next_line = lines[next_line_idx].strip()
|
188
|
-
if not (
|
189
|
-
next_line.startswith(
|
190
|
-
("return", "class ", "def ", "async def ", "@")
|
191
|
-
)
|
192
|
-
or next_line in ("pass", "break", "continue", "raise")
|
193
|
-
or (
|
194
|
-
next_line.startswith("#")
|
195
|
-
and any(
|
196
|
-
pattern in next_line
|
197
|
-
for pattern in (
|
198
|
-
"type:",
|
199
|
-
"noqa",
|
200
|
-
"nosec",
|
201
|
-
"pragma:",
|
202
|
-
"pylint:",
|
203
|
-
"mypy:",
|
204
|
-
)
|
205
|
-
)
|
206
|
-
)
|
207
|
-
):
|
208
|
-
continue
|
209
224
|
cleaned_lines.append(line)
|
210
|
-
|
211
|
-
|
212
|
-
|
225
|
+
|
226
|
+
return "\n".join(self._remove_trailing_empty_lines(cleaned_lines))
|
227
|
+
|
228
|
+
def _update_function_state(
|
229
|
+
self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
|
230
|
+
) -> None:
|
231
|
+
"""Update function tracking state based on current line."""
|
232
|
+
if stripped_line.startswith(("def ", "async def ")):
|
233
|
+
function_tracker["in_function"] = True
|
234
|
+
function_tracker["function_indent"] = len(line) - len(stripped_line)
|
235
|
+
elif self._is_function_end(line, stripped_line, function_tracker):
|
236
|
+
function_tracker["in_function"] = False
|
237
|
+
function_tracker["function_indent"] = 0
|
238
|
+
|
239
|
+
def _is_function_end(
|
240
|
+
self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
|
241
|
+
) -> bool:
|
242
|
+
"""Check if current line marks the end of a function."""
|
243
|
+
return (
|
244
|
+
function_tracker["in_function"]
|
245
|
+
and bool(line)
|
246
|
+
and (len(line) - len(stripped_line) <= function_tracker["function_indent"])
|
247
|
+
and (not stripped_line.startswith(("@", "#")))
|
248
|
+
)
|
249
|
+
|
250
|
+
def _should_skip_empty_line(
|
251
|
+
self,
|
252
|
+
line_idx: int,
|
253
|
+
lines: list[str],
|
254
|
+
cleaned_lines: list[str],
|
255
|
+
function_tracker: dict[str, t.Any],
|
256
|
+
) -> bool:
|
257
|
+
"""Determine if an empty line should be skipped."""
|
258
|
+
if line_idx > 0 and cleaned_lines and (not cleaned_lines[-1]):
|
259
|
+
return True
|
260
|
+
|
261
|
+
if function_tracker["in_function"]:
|
262
|
+
return self._should_skip_function_empty_line(line_idx, lines)
|
263
|
+
|
264
|
+
return False
|
265
|
+
|
266
|
+
def _should_skip_function_empty_line(self, line_idx: int, lines: list[str]) -> bool:
|
267
|
+
next_line_idx = line_idx + 1
|
268
|
+
if next_line_idx >= len(lines):
|
269
|
+
return False
|
270
|
+
next_line = lines[next_line_idx].strip()
|
271
|
+
return not self._is_significant_next_line(next_line)
|
272
|
+
|
273
|
+
def _is_significant_next_line(self, next_line: str) -> bool:
|
274
|
+
if next_line.startswith(("return", "class ", "def ", "async def ", "@")):
|
275
|
+
return True
|
276
|
+
if next_line in ("pass", "break", "continue", "raise"):
|
277
|
+
return True
|
278
|
+
|
279
|
+
return self._is_special_comment(next_line)
|
280
|
+
|
281
|
+
def _is_special_comment(self, line: str) -> bool:
|
282
|
+
if not line.startswith("#"):
|
283
|
+
return False
|
284
|
+
special_patterns = ("type:", "noqa", "nosec", "pragma:", "pylint:", "mypy:")
|
285
|
+
return any(pattern in line for pattern in special_patterns)
|
286
|
+
|
287
|
+
def _remove_trailing_empty_lines(self, lines: list[str]) -> list[str]:
|
288
|
+
while lines and (not lines[-1]):
|
289
|
+
lines.pop()
|
290
|
+
return lines
|
213
291
|
|
214
292
|
def reformat_code(self, code: str) -> str:
|
215
293
|
from crackerjack.errors import handle_error
|
@@ -335,27 +413,75 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
335
413
|
self, our_toml_config: dict[str, t.Any], pkg_toml_config: dict[str, t.Any]
|
336
414
|
) -> None:
|
337
415
|
for tool, settings in our_toml_config.get("tool", {}).items():
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
416
|
+
if tool not in pkg_toml_config["tool"]:
|
417
|
+
pkg_toml_config["tool"][tool] = {}
|
418
|
+
|
419
|
+
pkg_tool_config = pkg_toml_config["tool"][tool]
|
420
|
+
|
421
|
+
self._merge_tool_config(settings, pkg_tool_config, tool)
|
422
|
+
|
423
|
+
def _merge_tool_config(
|
424
|
+
self, our_config: dict[str, t.Any], pkg_config: dict[str, t.Any], tool: str
|
425
|
+
) -> None:
|
426
|
+
"""Recursively merge tool configuration, preserving existing project settings."""
|
427
|
+
for setting, value in our_config.items():
|
428
|
+
if isinstance(value, dict):
|
429
|
+
self._merge_nested_config(setting, value, pkg_config)
|
430
|
+
else:
|
431
|
+
self._merge_direct_config(setting, value, pkg_config)
|
432
|
+
|
433
|
+
def _merge_nested_config(
|
434
|
+
self, setting: str, value: dict[str, t.Any], pkg_config: dict[str, t.Any]
|
435
|
+
) -> None:
|
436
|
+
"""Handle nested configuration merging."""
|
437
|
+
if setting not in pkg_config:
|
438
|
+
pkg_config[setting] = {}
|
439
|
+
elif not isinstance(pkg_config[setting], dict):
|
440
|
+
pkg_config[setting] = {}
|
441
|
+
|
442
|
+
self._merge_tool_config(value, pkg_config[setting], "")
|
443
|
+
|
444
|
+
for k, v in value.items():
|
445
|
+
self._merge_nested_value(k, v, pkg_config[setting])
|
446
|
+
|
447
|
+
def _merge_nested_value(
|
448
|
+
self, key: str, value: t.Any, nested_config: dict[str, t.Any]
|
449
|
+
) -> None:
|
450
|
+
"""Merge individual nested values."""
|
451
|
+
if isinstance(value, str | list) and "crackerjack" in str(value):
|
452
|
+
nested_config[key] = self.swap_package_name(value)
|
453
|
+
elif self._is_mergeable_list(key, value):
|
454
|
+
existing = nested_config.get(key, [])
|
455
|
+
if isinstance(existing, list) and isinstance(value, list):
|
456
|
+
nested_config[key] = list(set(existing + value))
|
457
|
+
else:
|
458
|
+
nested_config[key] = value
|
459
|
+
elif key not in nested_config:
|
460
|
+
nested_config[key] = value
|
461
|
+
|
462
|
+
def _merge_direct_config(
|
463
|
+
self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
|
464
|
+
) -> None:
|
465
|
+
"""Handle direct configuration merging."""
|
466
|
+
if isinstance(value, str | list) and "crackerjack" in str(value):
|
467
|
+
pkg_config[setting] = self.swap_package_name(value)
|
468
|
+
elif self._is_mergeable_list(setting, value):
|
469
|
+
existing = pkg_config.get(setting, [])
|
470
|
+
if isinstance(existing, list) and isinstance(value, list):
|
471
|
+
pkg_config[setting] = list(set(existing + value))
|
472
|
+
else:
|
473
|
+
pkg_config[setting] = value
|
474
|
+
elif setting not in pkg_config:
|
475
|
+
pkg_config[setting] = value
|
476
|
+
|
477
|
+
def _is_mergeable_list(self, key: str, value: t.Any) -> bool:
|
478
|
+
return key in (
|
479
|
+
"exclude-deps",
|
480
|
+
"exclude",
|
481
|
+
"excluded",
|
482
|
+
"skips",
|
483
|
+
"ignore",
|
484
|
+
) and isinstance(value, list)
|
359
485
|
|
360
486
|
def _update_python_version(
|
361
487
|
self, our_toml_config: dict[str, t.Any], pkg_toml_config: dict[str, t.Any]
|
@@ -520,11 +646,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
520
646
|
if self.pkg_path.stem == "crackerjack" and options.update_precommit:
|
521
647
|
self.execute_command(["pre-commit", "autoupdate"])
|
522
648
|
|
523
|
-
def _run_interactive_hooks(self, options: t.Any) -> None:
|
524
|
-
if options.interactive:
|
525
|
-
for hook in interactive_hooks:
|
526
|
-
self.project_manager.run_interactive(hook)
|
527
|
-
|
528
649
|
def _clean_project(self, options: t.Any) -> None:
|
529
650
|
if options.clean:
|
530
651
|
if self.pkg_dir:
|
@@ -655,7 +776,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
655
776
|
self._setup_package()
|
656
777
|
self._update_project(options)
|
657
778
|
self._update_precommit(options)
|
658
|
-
self._run_interactive_hooks(options)
|
659
779
|
self._clean_project(options)
|
660
780
|
if not options.skip_hooks:
|
661
781
|
self.project_manager.run_pre_commit()
|
crackerjack/interactive.py
CHANGED
@@ -260,11 +260,26 @@ class InteractiveCLI:
|
|
260
260
|
|
261
261
|
def run_interactive(self) -> None:
|
262
262
|
self.console.clear()
|
263
|
+
layout = self._setup_interactive_layout()
|
264
|
+
progress_tracker = self._create_progress_tracker()
|
265
|
+
with Live(layout, refresh_per_second=4, screen=True) as live:
|
266
|
+
try:
|
267
|
+
self._execute_workflow_loop(layout, progress_tracker, live)
|
268
|
+
self._display_final_summary(layout)
|
269
|
+
except KeyboardInterrupt:
|
270
|
+
self._handle_user_interruption(layout)
|
271
|
+
self.console.print("\nWorkflow Status:")
|
272
|
+
self.workflow.display_task_tree()
|
273
|
+
|
274
|
+
def _setup_interactive_layout(self) -> Layout:
|
263
275
|
layout = self.setup_layout()
|
264
276
|
layout["header"].update(
|
265
277
|
Panel("Crackerjack Interactive Mode", style="bold cyan", box=ROUNDED)
|
266
278
|
)
|
267
279
|
layout["footer"].update(Panel("Press Ctrl+C to exit", style="dim", box=ROUNDED))
|
280
|
+
return layout
|
281
|
+
|
282
|
+
def _create_progress_tracker(self) -> dict[str, t.Any]:
|
268
283
|
progress = Progress(
|
269
284
|
SpinnerColumn(),
|
270
285
|
TextColumn("[bold blue]{task.description}"),
|
@@ -274,72 +289,110 @@ class InteractiveCLI:
|
|
274
289
|
)
|
275
290
|
total_tasks = len(self.workflow.tasks)
|
276
291
|
progress_task = progress.add_task("Running workflow", total=total_tasks)
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
292
|
+
|
293
|
+
return {
|
294
|
+
"progress": progress,
|
295
|
+
"progress_task": progress_task,
|
296
|
+
"completed_tasks": 0,
|
297
|
+
}
|
298
|
+
|
299
|
+
def _execute_workflow_loop(
|
300
|
+
self, layout: Layout, progress_tracker: dict[str, t.Any], live: Live
|
301
|
+
) -> None:
|
302
|
+
"""Execute the main workflow loop."""
|
303
|
+
while not self.workflow.all_tasks_completed():
|
304
|
+
layout["tasks"].update(self.show_task_table())
|
305
|
+
next_task = self.workflow.get_next_task()
|
306
|
+
|
307
|
+
if not next_task:
|
308
|
+
break
|
309
|
+
|
310
|
+
if self._should_execute_task(layout, next_task, live):
|
311
|
+
self._execute_task(layout, next_task, progress_tracker)
|
312
|
+
else:
|
313
|
+
next_task.skip()
|
314
|
+
|
315
|
+
def _should_execute_task(self, layout: Layout, task: Task, live: Live) -> bool:
|
316
|
+
layout["details"].update(self.show_task_status(task))
|
317
|
+
live.stop()
|
318
|
+
should_run = Confirm.ask(f"Run task '{task.name}'?", default=True)
|
319
|
+
live.start()
|
320
|
+
return should_run
|
321
|
+
|
322
|
+
def _execute_task(
|
323
|
+
self, layout: Layout, task: Task, progress_tracker: dict[str, t.Any]
|
324
|
+
) -> None:
|
325
|
+
"""Execute a single task and update progress."""
|
326
|
+
task.start()
|
327
|
+
layout["details"].update(self.show_task_status(task))
|
328
|
+
time.sleep(1)
|
329
|
+
|
330
|
+
success = self._simulate_task_execution()
|
331
|
+
|
332
|
+
if success:
|
333
|
+
task.complete()
|
334
|
+
progress_tracker["completed_tasks"] += 1
|
335
|
+
else:
|
336
|
+
error = self._create_task_error(task.name)
|
337
|
+
task.fail(error)
|
338
|
+
|
339
|
+
progress_tracker["progress"].update(
|
340
|
+
progress_tracker["progress_task"],
|
341
|
+
completed=progress_tracker["completed_tasks"],
|
342
|
+
)
|
343
|
+
layout["details"].update(self.show_task_status(task))
|
344
|
+
|
345
|
+
def _simulate_task_execution(self) -> bool:
|
346
|
+
import random
|
347
|
+
|
348
|
+
return random.choice([True, True, True, False])
|
349
|
+
|
350
|
+
def _create_task_error(self, task_name: str) -> t.Any:
|
351
|
+
from .errors import ExecutionError
|
352
|
+
|
353
|
+
return ExecutionError(
|
354
|
+
message=f"Task '{task_name}' failed",
|
355
|
+
error_code=ErrorCode.COMMAND_EXECUTION_ERROR,
|
356
|
+
details="This is a simulated failure for demonstration.",
|
357
|
+
recovery=f"Retry the '{task_name}' task.",
|
358
|
+
)
|
359
|
+
|
360
|
+
def _display_final_summary(self, layout: Layout) -> None:
|
361
|
+
layout["tasks"].update(self.show_task_table())
|
362
|
+
task_counts = self._count_tasks_by_status()
|
363
|
+
summary = Panel(
|
364
|
+
f"Workflow completed!\n\n"
|
365
|
+
f"[green]✅ Successful tasks: {task_counts['successful']}[/green]\n"
|
366
|
+
f"[red]❌ Failed tasks: {task_counts['failed']}[/red]\n"
|
367
|
+
f"[blue]⏩ Skipped tasks: {task_counts['skipped']}[/blue]",
|
368
|
+
title="Summary",
|
369
|
+
border_style="cyan",
|
370
|
+
)
|
371
|
+
layout["details"].update(summary)
|
372
|
+
|
373
|
+
def _count_tasks_by_status(self) -> dict[str, int]:
|
374
|
+
return {
|
375
|
+
"successful": sum(
|
376
|
+
1
|
377
|
+
for task in self.workflow.tasks.values()
|
378
|
+
if task.status == TaskStatus.SUCCESS
|
379
|
+
),
|
380
|
+
"failed": sum(
|
381
|
+
1
|
382
|
+
for task in self.workflow.tasks.values()
|
383
|
+
if task.status == TaskStatus.FAILED
|
384
|
+
),
|
385
|
+
"skipped": sum(
|
386
|
+
1
|
387
|
+
for task in self.workflow.tasks.values()
|
388
|
+
if task.status == TaskStatus.SKIPPED
|
389
|
+
),
|
390
|
+
}
|
391
|
+
|
392
|
+
def _handle_user_interruption(self, layout: Layout) -> None:
|
393
|
+
layout["footer"].update(
|
394
|
+
Panel("Interrupted by user", style="yellow", box=ROUNDED)
|
395
|
+
)
|
343
396
|
|
344
397
|
def ask_for_file(
|
345
398
|
self, prompt: str, directory: Path, default: str | None = None
|
crackerjack/pyproject.toml
CHANGED
@@ -4,7 +4,7 @@ requires = [ "pdm-backend" ]
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "crackerjack"
|
7
|
-
version = "0.20.
|
7
|
+
version = "0.20.20"
|
8
8
|
description = "Crackerjack: code quality toolkit"
|
9
9
|
readme = "README.md"
|
10
10
|
keywords = [
|
@@ -43,14 +43,14 @@ classifiers = [
|
|
43
43
|
dependencies = [
|
44
44
|
"autotyping>=24.9",
|
45
45
|
"keyring>=25.6",
|
46
|
-
"pdm>=2.
|
46
|
+
"pdm>=2.25.3",
|
47
47
|
"pdm-bump>=0.9.12",
|
48
48
|
"pre-commit>=4.2",
|
49
|
-
"pydantic>=2.11.
|
50
|
-
"pytest>=8.4",
|
49
|
+
"pydantic>=2.11.7",
|
50
|
+
"pytest>=8.4.1",
|
51
51
|
"pytest-asyncio>=1",
|
52
52
|
"pytest-benchmark>=5.1",
|
53
|
-
"pytest-cov>=6.2",
|
53
|
+
"pytest-cov>=6.2.1",
|
54
54
|
"pytest-mock>=3.14.1",
|
55
55
|
"pytest-timeout>=2.4",
|
56
56
|
"pytest-xdist>=3.7",
|
@@ -58,7 +58,7 @@ dependencies = [
|
|
58
58
|
"rich>=14",
|
59
59
|
"tomli-w>=1.2",
|
60
60
|
"typer>=0.16",
|
61
|
-
"uv>=0.7.
|
61
|
+
"uv>=0.7.15",
|
62
62
|
]
|
63
63
|
urls.documentation = "https://github.com/lesleslie/crackerjack"
|
64
64
|
urls.homepage = "https://github.com/lesleslie/crackerjack"
|
@@ -89,6 +89,7 @@ lint.ignore = [
|
|
89
89
|
"D105",
|
90
90
|
"D106",
|
91
91
|
"D107",
|
92
|
+
"E402",
|
92
93
|
"F821",
|
93
94
|
"UP040",
|
94
95
|
]
|
@@ -221,6 +222,8 @@ exclude-deps = [
|
|
221
222
|
"google-crc32c",
|
222
223
|
"pytest-timeout",
|
223
224
|
"keyring",
|
225
|
+
"inflection",
|
226
|
+
"pydantic-settings",
|
224
227
|
]
|
225
228
|
|
226
229
|
[tool.refurb]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: crackerjack
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.21.0
|
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>
|
@@ -24,14 +24,14 @@ Project-URL: repository, https://github.com/lesleslie/crackerjack
|
|
24
24
|
Requires-Python: >=3.13
|
25
25
|
Requires-Dist: autotyping>=24.9
|
26
26
|
Requires-Dist: keyring>=25.6
|
27
|
-
Requires-Dist: pdm>=2.
|
27
|
+
Requires-Dist: pdm>=2.25.3
|
28
28
|
Requires-Dist: pdm-bump>=0.9.12
|
29
29
|
Requires-Dist: pre-commit>=4.2
|
30
|
-
Requires-Dist: pydantic>=2.11.
|
31
|
-
Requires-Dist: pytest>=8.4
|
30
|
+
Requires-Dist: pydantic>=2.11.7
|
31
|
+
Requires-Dist: pytest>=8.4.1
|
32
32
|
Requires-Dist: pytest-asyncio>=1
|
33
33
|
Requires-Dist: pytest-benchmark>=5.1
|
34
|
-
Requires-Dist: pytest-cov>=6.2
|
34
|
+
Requires-Dist: pytest-cov>=6.2.1
|
35
35
|
Requires-Dist: pytest-mock>=3.14.1
|
36
36
|
Requires-Dist: pytest-timeout>=2.4
|
37
37
|
Requires-Dist: pytest-xdist>=3.7
|
@@ -39,7 +39,7 @@ Requires-Dist: pyyaml>=6.0.2
|
|
39
39
|
Requires-Dist: rich>=14
|
40
40
|
Requires-Dist: tomli-w>=1.2
|
41
41
|
Requires-Dist: typer>=0.16
|
42
|
-
Requires-Dist: uv>=0.7.
|
42
|
+
Requires-Dist: uv>=0.7.15
|
43
43
|
Description-Content-Type: text/markdown
|
44
44
|
|
45
45
|
# Crackerjack: Elevate Your Python Development
|
@@ -1,11 +1,11 @@
|
|
1
|
-
crackerjack-0.
|
2
|
-
crackerjack-0.
|
3
|
-
crackerjack-0.
|
4
|
-
crackerjack-0.
|
1
|
+
crackerjack-0.21.0.dist-info/METADATA,sha256=5sb-8guKpbw-RU1VKg-VttPLTQTefNrvlNhQgEW3kTc,26414
|
2
|
+
crackerjack-0.21.0.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
3
|
+
crackerjack-0.21.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
4
|
+
crackerjack-0.21.0.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
|
5
5
|
crackerjack/.gitignore,sha256=FOLrDV-tuHpwof310aOi6cWOkJXeVI_gvWvg_paDzs4,199
|
6
6
|
crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
|
7
7
|
crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
|
8
|
-
crackerjack/.pre-commit-config.yaml,sha256=
|
8
|
+
crackerjack/.pre-commit-config.yaml,sha256=glBcQ4nwC28t4Ier2E69bUg8pNQ9YyCtHmD-fV6bRm0,3076
|
9
9
|
crackerjack/.pytest_cache/.gitignore,sha256=Ptcxtl0GFQwTji2tsL4Gl1UIiKa0frjEXsya26i46b0,37
|
10
10
|
crackerjack/.pytest_cache/CACHEDIR.TAG,sha256=N9yI75oKvt2-gQU6bdj9-xOvthMEXqHrSlyBWnSjveQ,191
|
11
11
|
crackerjack/.pytest_cache/README.md,sha256=c_1vzN2ALEGaay2YPWwxc7fal1WKxLWJ7ewt_kQ9ua0,302
|
@@ -23,7 +23,7 @@ crackerjack/.ruff_cache/0.11.12/16869036553936192448,sha256=pYYUCDrYh7fPq8xkFLxv
|
|
23
23
|
crackerjack/.ruff_cache/0.11.12/1867267426380906393,sha256=2w4M0Lrjd9flwuq6uJxehTbm7FVUcK5sL2sz1gS2Yvo,256
|
24
24
|
crackerjack/.ruff_cache/0.11.12/4240757255861806333,sha256=uph5uIRG-XnF7ywAEcCxqqgIkWALPCvJFcwCgnNfTI4,77
|
25
25
|
crackerjack/.ruff_cache/0.11.12/4441409093023629623,sha256=eHrESew3XCFJ2WqmKvtGLO1r4mY5Q_mv7yGlDmM1sSc,153
|
26
|
-
crackerjack/.ruff_cache/0.11.13/1867267426380906393,sha256=
|
26
|
+
crackerjack/.ruff_cache/0.11.13/1867267426380906393,sha256=XRpZCU9UqBNycXaVuXEfG6ReYZREBUUjwV46ibS7dlk,256
|
27
27
|
crackerjack/.ruff_cache/0.11.13/4240757255861806333,sha256=l35TwAYyTusgJgyePvfP4_CCllPs1sWapEiLFZw8chQ,83
|
28
28
|
crackerjack/.ruff_cache/0.11.2/4070660268492669020,sha256=FTRTUmvj6nZw_QQBp_WHI-h3_iqRejzL39api-9wTvs,224
|
29
29
|
crackerjack/.ruff_cache/0.11.3/9818742842212983150,sha256=U-4mT__a-OljovvAJvv5M6X7TCMa3dReLXx3kTNGgwU,224
|
@@ -32,6 +32,8 @@ crackerjack/.ruff_cache/0.11.6/3557596832929915217,sha256=yR2iXWDkSHVRw2eTiaCE8E
|
|
32
32
|
crackerjack/.ruff_cache/0.11.7/10386934055395314831,sha256=lBNwN5zAgM4OzbkXIOzCczUtfooATrD10htj9ASlFkc,224
|
33
33
|
crackerjack/.ruff_cache/0.11.7/3557596832929915217,sha256=fKlwUbsvT3YIKV6UR-aA_i64lLignWeVfVu-MMmVbU0,207
|
34
34
|
crackerjack/.ruff_cache/0.11.8/530407680854991027,sha256=xAMAL3Vu_HR6M-h5ojCTaak0By5ii8u-14pXULLgLqw,224
|
35
|
+
crackerjack/.ruff_cache/0.12.0/5056746222905752453,sha256=MqrIT5qymJcgAOBZyn-TvYoGCFfDFCgN9IwSULq8n14,256
|
36
|
+
crackerjack/.ruff_cache/0.12.1/5056746222905752453,sha256=DxPQ70wEGAhDamfTzZgdH_n7fxBhsV_59f3jApsFXu0,256
|
35
37
|
crackerjack/.ruff_cache/0.2.0/10047773857155985907,sha256=j9LNa_RQ4Plor7go1uTYgz17cEENKvZQ-dP6b9MX0ik,248
|
36
38
|
crackerjack/.ruff_cache/0.2.1/8522267973936635051,sha256=u_aPBMibtAp_iYvLwR88GMAECMcIgHezxMyuapmU2P4,248
|
37
39
|
crackerjack/.ruff_cache/0.2.2/18053836298936336950,sha256=Xb_ebP0pVuUfSqPEZKlhQ70so_vqkEfMYpuHQ06iR5U,248
|
@@ -60,10 +62,10 @@ crackerjack/.ruff_cache/0.9.9/12813592349865671909,sha256=tmr8_vhRD2OxsVuMfbJPdT
|
|
60
62
|
crackerjack/.ruff_cache/0.9.9/8843823720003377982,sha256=e4ymkXfQsUg5e_mtO34xTsaTvs1uA3_fI216Qq9qCAM,136
|
61
63
|
crackerjack/.ruff_cache/CACHEDIR.TAG,sha256=WVMVbX4MVkpCclExbq8m-IcOZIOuIZf5FrYw5Pk-Ma4,43
|
62
64
|
crackerjack/__init__.py,sha256=8tBSPAru_YDuPpjz05cL7pNbZjYFoRT_agGd_FWa3gY,839
|
63
|
-
crackerjack/__main__.py,sha256=
|
64
|
-
crackerjack/crackerjack.py,sha256=
|
65
|
+
crackerjack/__main__.py,sha256=AknITUlFjq3YUK9s2xeL62dM0GN82JBQyDkPzQ_hCUg,6561
|
66
|
+
crackerjack/crackerjack.py,sha256=pENFT19EqA4DBro9YtmP8avhfQbPljMkRF5MXn0eybo,30900
|
65
67
|
crackerjack/errors.py,sha256=QEPtVuMtKtQHuawgr1ToMaN1KbUg5h9-4mS33YB5Znk,4062
|
66
|
-
crackerjack/interactive.py,sha256=
|
68
|
+
crackerjack/interactive.py,sha256=5KXKSvWKttLvHcI1L4VEDc3Rb-ZpHBOl_Qr7lhD-O4Q,16262
|
67
69
|
crackerjack/py313.py,sha256=buYE7LO11Q64ffowEhTZRFQoAGj_8sg3DTlZuv8M9eo,5890
|
68
|
-
crackerjack/pyproject.toml,sha256=
|
69
|
-
crackerjack-0.
|
70
|
+
crackerjack/pyproject.toml,sha256=kiN_CuXFS307ca2CvLt-nDvZbFgouTxb1uIsSEkdIDk,4989
|
71
|
+
crackerjack-0.21.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|