crackerjack 0.20.3__py3-none-any.whl → 0.20.10__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/.ruff_cache/0.11.13/1867267426380906393 +0 -0
- crackerjack/.ruff_cache/0.11.13/4240757255861806333 +0 -0
- crackerjack/__init__.py +0 -2
- crackerjack/__main__.py +2 -11
- crackerjack/crackerjack.py +186 -889
- crackerjack/errors.py +0 -20
- crackerjack/interactive.py +38 -121
- crackerjack/py313.py +10 -50
- crackerjack/pyproject.toml +3 -1
- {crackerjack-0.20.3.dist-info → crackerjack-0.20.10.dist-info}/METADATA +2 -1
- {crackerjack-0.20.3.dist-info → crackerjack-0.20.10.dist-info}/RECORD +14 -13
- {crackerjack-0.20.3.dist-info → crackerjack-0.20.10.dist-info}/WHEEL +0 -0
- {crackerjack-0.20.3.dist-info → crackerjack-0.20.10.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.20.3.dist-info → crackerjack-0.20.10.dist-info}/licenses/LICENSE +0 -0
crackerjack/errors.py
CHANGED
@@ -1,15 +1,7 @@
|
|
1
|
-
"""Error handling module for Crackerjack.
|
2
|
-
|
3
|
-
This module defines a structured error system with error codes, detailed error messages,
|
4
|
-
and recovery suggestions. It provides a consistent way to handle errors throughout
|
5
|
-
the application.
|
6
|
-
"""
|
7
|
-
|
8
1
|
import sys
|
9
2
|
import typing as t
|
10
3
|
from enum import Enum
|
11
4
|
from pathlib import Path
|
12
|
-
|
13
5
|
from rich.console import Console
|
14
6
|
from rich.panel import Panel
|
15
7
|
|
@@ -19,34 +11,27 @@ class ErrorCode(Enum):
|
|
19
11
|
CONFIG_PARSE_ERROR = 1002
|
20
12
|
INVALID_CONFIG = 1003
|
21
13
|
MISSING_CONFIG_FIELD = 1004
|
22
|
-
|
23
14
|
COMMAND_EXECUTION_ERROR = 2001
|
24
15
|
COMMAND_TIMEOUT = 2002
|
25
16
|
EXTERNAL_TOOL_ERROR = 2003
|
26
17
|
PDM_INSTALL_ERROR = 2004
|
27
18
|
PRE_COMMIT_ERROR = 2005
|
28
|
-
|
29
19
|
TEST_EXECUTION_ERROR = 3001
|
30
20
|
TEST_FAILURE = 3002
|
31
21
|
BENCHMARK_REGRESSION = 3003
|
32
|
-
|
33
22
|
BUILD_ERROR = 4001
|
34
23
|
PUBLISH_ERROR = 4002
|
35
24
|
VERSION_BUMP_ERROR = 4003
|
36
25
|
AUTHENTICATION_ERROR = 4004
|
37
|
-
|
38
26
|
GIT_COMMAND_ERROR = 5001
|
39
27
|
PULL_REQUEST_ERROR = 5002
|
40
28
|
COMMIT_ERROR = 5003
|
41
|
-
|
42
29
|
FILE_NOT_FOUND = 6001
|
43
30
|
PERMISSION_ERROR = 6002
|
44
31
|
FILE_READ_ERROR = 6003
|
45
32
|
FILE_WRITE_ERROR = 6004
|
46
|
-
|
47
33
|
CODE_CLEANING_ERROR = 7001
|
48
34
|
FORMATTING_ERROR = 7002
|
49
|
-
|
50
35
|
UNKNOWN_ERROR = 9001
|
51
36
|
NOT_IMPLEMENTED = 9002
|
52
37
|
UNEXPECTED_ERROR = 9999
|
@@ -122,15 +107,12 @@ def handle_error(
|
|
122
107
|
else:
|
123
108
|
title = f"❌ Error {error.error_code.value}: {error.error_code.name}"
|
124
109
|
content = [error.message]
|
125
|
-
|
126
110
|
if verbose and error.details:
|
127
111
|
content.extend(("\n[bold]Details:[/bold]", str(error.details)))
|
128
|
-
|
129
112
|
if error.recovery:
|
130
113
|
content.extend(
|
131
114
|
("\n[bold green]Recovery suggestion:[/bold green]", str(error.recovery))
|
132
115
|
)
|
133
|
-
|
134
116
|
console.print(
|
135
117
|
Panel(
|
136
118
|
"\n".join(content),
|
@@ -140,7 +122,6 @@ def handle_error(
|
|
140
122
|
expand=False,
|
141
123
|
)
|
142
124
|
)
|
143
|
-
|
144
125
|
if exit_on_error:
|
145
126
|
sys.exit(error.exit_code)
|
146
127
|
|
@@ -167,7 +148,6 @@ def check_command_result(
|
|
167
148
|
details = f"Command '{command}' failed with return code {result.returncode}."
|
168
149
|
if stderr:
|
169
150
|
details += f"\nStandard error output:\n{stderr}"
|
170
|
-
|
171
151
|
raise ExecutionError(
|
172
152
|
message=error_message,
|
173
153
|
error_code=error_code,
|
crackerjack/interactive.py
CHANGED
@@ -1,14 +1,7 @@
|
|
1
|
-
"""Interactive CLI module using Rich for enhanced user experience.
|
2
|
-
|
3
|
-
This module provides an enhanced interactive CLI experience using Rich library components.
|
4
|
-
It includes progress tracking, interactive prompts, and detailed error reporting.
|
5
|
-
"""
|
6
|
-
|
7
1
|
import time
|
8
2
|
import typing as t
|
9
3
|
from enum import Enum, auto
|
10
4
|
from pathlib import Path
|
11
|
-
|
12
5
|
from rich.box import ROUNDED
|
13
6
|
from rich.console import Console
|
14
7
|
from rich.layout import Layout
|
@@ -25,7 +18,6 @@ from rich.prompt import Confirm, Prompt
|
|
25
18
|
from rich.table import Table
|
26
19
|
from rich.text import Text
|
27
20
|
from rich.tree import Tree
|
28
|
-
|
29
21
|
from .errors import CrackerjackError, ErrorCode, handle_error
|
30
22
|
|
31
23
|
|
@@ -39,10 +31,7 @@ class TaskStatus(Enum):
|
|
39
31
|
|
40
32
|
class Task:
|
41
33
|
def __init__(
|
42
|
-
self,
|
43
|
-
name: str,
|
44
|
-
description: str,
|
45
|
-
dependencies: list["Task"] | None = None,
|
34
|
+
self, name: str, description: str, dependencies: list["Task"] | None = None
|
46
35
|
) -> None:
|
47
36
|
self.name = name
|
48
37
|
self.description = description
|
@@ -77,8 +66,10 @@ class Task:
|
|
77
66
|
|
78
67
|
def can_run(self) -> bool:
|
79
68
|
return all(
|
80
|
-
|
81
|
-
|
69
|
+
(
|
70
|
+
dep.status in (TaskStatus.SUCCESS, TaskStatus.SKIPPED)
|
71
|
+
for dep in self.dependencies
|
72
|
+
)
|
82
73
|
)
|
83
74
|
|
84
75
|
def __str__(self) -> str:
|
@@ -92,10 +83,7 @@ class WorkflowManager:
|
|
92
83
|
self.current_task: Task | None = None
|
93
84
|
|
94
85
|
def add_task(
|
95
|
-
self,
|
96
|
-
name: str,
|
97
|
-
description: str,
|
98
|
-
dependencies: list[str] | None = None,
|
86
|
+
self, name: str, description: str, dependencies: list[str] | None = None
|
99
87
|
) -> Task:
|
100
88
|
dep_tasks = []
|
101
89
|
if dependencies:
|
@@ -103,7 +91,6 @@ class WorkflowManager:
|
|
103
91
|
if dep_name not in self.tasks:
|
104
92
|
raise ValueError(f"Dependency task '{dep_name}' not found")
|
105
93
|
dep_tasks.append(self.tasks[dep_name])
|
106
|
-
|
107
94
|
task = Task(name, description, dep_tasks)
|
108
95
|
self.tasks[name] = task
|
109
96
|
return task
|
@@ -113,21 +100,23 @@ class WorkflowManager:
|
|
113
100
|
if (
|
114
101
|
task.status == TaskStatus.PENDING
|
115
102
|
and task.can_run()
|
116
|
-
and task != self.current_task
|
103
|
+
and (task != self.current_task)
|
117
104
|
):
|
118
105
|
return task
|
119
106
|
return None
|
120
107
|
|
121
108
|
def all_tasks_completed(self) -> bool:
|
122
109
|
return all(
|
123
|
-
|
124
|
-
|
110
|
+
(
|
111
|
+
task.status
|
112
|
+
in (TaskStatus.SUCCESS, TaskStatus.FAILED, TaskStatus.SKIPPED)
|
113
|
+
for task in self.tasks.values()
|
114
|
+
)
|
125
115
|
)
|
126
116
|
|
127
117
|
def run_task(self, task: Task, func: t.Callable[[], t.Any]) -> bool:
|
128
118
|
self.current_task = task
|
129
119
|
task.start()
|
130
|
-
|
131
120
|
try:
|
132
121
|
func()
|
133
122
|
task.complete()
|
@@ -151,11 +140,9 @@ class WorkflowManager:
|
|
151
140
|
|
152
141
|
def display_task_tree(self) -> None:
|
153
142
|
tree = Tree("Workflow")
|
154
|
-
|
155
143
|
for task in self.tasks.values():
|
156
144
|
if not task.dependencies:
|
157
145
|
self._add_task_to_tree(task, tree)
|
158
|
-
|
159
146
|
self.console.print(tree)
|
160
147
|
|
161
148
|
def _add_task_to_tree(self, task: Task, parent: Tree) -> None:
|
@@ -169,9 +156,7 @@ class WorkflowManager:
|
|
169
156
|
status = "[blue]⏩[/blue]"
|
170
157
|
else:
|
171
158
|
status = "[grey]⏸️[/grey]"
|
172
|
-
|
173
159
|
branch = parent.add(f"{status} {task.name} - {task.description}")
|
174
|
-
|
175
160
|
for dependent in self.tasks.values():
|
176
161
|
if task in dependent.dependencies:
|
177
162
|
self._add_task_to_tree(dependent, branch)
|
@@ -186,14 +171,12 @@ class InteractiveCLI:
|
|
186
171
|
title = Text("Crackerjack", style="bold cyan")
|
187
172
|
version_text = Text(f"v{version}", style="dim cyan")
|
188
173
|
subtitle = Text("Your Python project management toolkit", style="italic")
|
189
|
-
|
190
174
|
panel = Panel(
|
191
175
|
f"{title} {version_text}\n{subtitle}",
|
192
176
|
box=ROUNDED,
|
193
177
|
border_style="cyan",
|
194
178
|
expand=False,
|
195
179
|
)
|
196
|
-
|
197
180
|
self.console.print(panel)
|
198
181
|
self.console.print()
|
199
182
|
|
@@ -213,18 +196,14 @@ class InteractiveCLI:
|
|
213
196
|
|
214
197
|
def setup_layout(self) -> Layout:
|
215
198
|
layout = Layout()
|
216
|
-
|
217
199
|
layout.split(
|
218
200
|
Layout(name="header", size=3),
|
219
201
|
Layout(name="main"),
|
220
202
|
Layout(name="footer", size=3),
|
221
203
|
)
|
222
|
-
|
223
204
|
layout["main"].split_row(
|
224
|
-
Layout(name="tasks", ratio=1),
|
225
|
-
Layout(name="details", ratio=2),
|
205
|
+
Layout(name="tasks", ratio=1), Layout(name="details", ratio=2)
|
226
206
|
)
|
227
|
-
|
228
207
|
return layout
|
229
208
|
|
230
209
|
def show_task_status(self, task: Task) -> Panel:
|
@@ -243,25 +222,16 @@ class InteractiveCLI:
|
|
243
222
|
else:
|
244
223
|
status = "[grey]⏸️ Pending[/grey]"
|
245
224
|
style = "dim"
|
246
|
-
|
247
225
|
duration = task.duration
|
248
226
|
duration_text = f"Duration: {duration:.2f}s" if duration else ""
|
249
|
-
|
250
227
|
content = f"{task.name}: {task.description}\nStatus: {status}\n{duration_text}"
|
251
|
-
|
252
228
|
if task.error:
|
253
229
|
content += f"\n[red]Error: {task.error.message}[/red]"
|
254
230
|
if task.error.details:
|
255
231
|
content += f"\n[dim red]Details: {task.error.details}[/dim red]"
|
256
232
|
if task.error.recovery:
|
257
233
|
content += f"\n[yellow]Recovery: {task.error.recovery}[/yellow]"
|
258
|
-
|
259
|
-
return Panel(
|
260
|
-
content,
|
261
|
-
title=task.name,
|
262
|
-
border_style=style,
|
263
|
-
expand=False,
|
264
|
-
)
|
234
|
+
return Panel(content, title=task.name, border_style=style, expand=False)
|
265
235
|
|
266
236
|
def show_task_table(self) -> Table:
|
267
237
|
table = Table(
|
@@ -270,12 +240,10 @@ class InteractiveCLI:
|
|
270
240
|
show_header=True,
|
271
241
|
header_style="bold cyan",
|
272
242
|
)
|
273
|
-
|
274
243
|
table.add_column("Task", style="cyan")
|
275
244
|
table.add_column("Status")
|
276
245
|
table.add_column("Duration")
|
277
246
|
table.add_column("Dependencies")
|
278
|
-
|
279
247
|
for task in self.workflow.tasks.values():
|
280
248
|
if task.status == TaskStatus.RUNNING:
|
281
249
|
status = "[yellow]⏳ Running[/yellow]"
|
@@ -287,36 +255,19 @@ class InteractiveCLI:
|
|
287
255
|
status = "[blue]⏩ Skipped[/blue]"
|
288
256
|
else:
|
289
257
|
status = "[grey]⏸️ Pending[/grey]"
|
290
|
-
|
291
258
|
duration = task.duration
|
292
259
|
duration_text = f"{duration:.2f}s" if duration else "-"
|
293
|
-
|
294
|
-
deps = ", ".join(dep.name for dep in task.dependencies) or "-"
|
295
|
-
|
260
|
+
deps = ", ".join((dep.name for dep in task.dependencies)) or "-"
|
296
261
|
table.add_row(task.name, status, duration_text, deps)
|
297
|
-
|
298
262
|
return table
|
299
263
|
|
300
264
|
def run_interactive(self) -> None:
|
301
265
|
self.console.clear()
|
302
266
|
layout = self.setup_layout()
|
303
|
-
|
304
267
|
layout["header"].update(
|
305
|
-
Panel(
|
306
|
-
"Crackerjack Interactive Mode",
|
307
|
-
style="bold cyan",
|
308
|
-
box=ROUNDED,
|
309
|
-
)
|
310
|
-
)
|
311
|
-
|
312
|
-
layout["footer"].update(
|
313
|
-
Panel(
|
314
|
-
"Press Ctrl+C to exit",
|
315
|
-
style="dim",
|
316
|
-
box=ROUNDED,
|
317
|
-
)
|
268
|
+
Panel("Crackerjack Interactive Mode", style="bold cyan", box=ROUNDED)
|
318
269
|
)
|
319
|
-
|
270
|
+
layout["footer"].update(Panel("Press Ctrl+C to exit", style="dim", box=ROUNDED))
|
320
271
|
progress = Progress(
|
321
272
|
SpinnerColumn(),
|
322
273
|
TextColumn("[bold blue]{task.description}"),
|
@@ -324,41 +275,31 @@ class InteractiveCLI:
|
|
324
275
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
325
276
|
TimeElapsedColumn(),
|
326
277
|
)
|
327
|
-
|
328
278
|
total_tasks = len(self.workflow.tasks)
|
329
279
|
progress_task = progress.add_task("Running workflow", total=total_tasks)
|
330
280
|
completed_tasks = 0
|
331
|
-
|
332
281
|
with Live(layout, refresh_per_second=4, screen=True) as live:
|
333
282
|
try:
|
334
283
|
while not self.workflow.all_tasks_completed():
|
335
284
|
layout["tasks"].update(self.show_task_table())
|
336
|
-
|
337
285
|
next_task = self.workflow.get_next_task()
|
338
286
|
if not next_task:
|
339
287
|
break
|
340
|
-
|
341
288
|
layout["details"].update(self.show_task_status(next_task))
|
342
|
-
|
343
289
|
live.stop()
|
344
290
|
should_run = Confirm.ask(
|
345
|
-
f"Run task '{next_task.name}'?",
|
346
|
-
default=True,
|
291
|
+
f"Run task '{next_task.name}'?", default=True
|
347
292
|
)
|
348
293
|
live.start()
|
349
|
-
|
350
294
|
if not should_run:
|
351
295
|
next_task.skip()
|
352
296
|
continue
|
353
|
-
|
354
297
|
next_task.start()
|
355
298
|
layout["details"].update(self.show_task_status(next_task))
|
356
299
|
time.sleep(1)
|
357
|
-
|
358
300
|
import random
|
359
301
|
|
360
302
|
success = random.choice([True, True, True, False])
|
361
|
-
|
362
303
|
if success:
|
363
304
|
next_task.complete(True)
|
364
305
|
completed_tasks += 1
|
@@ -372,48 +313,40 @@ class InteractiveCLI:
|
|
372
313
|
recovery=f"Retry the '{next_task.name}' task.",
|
373
314
|
)
|
374
315
|
next_task.fail(error)
|
375
|
-
|
376
316
|
progress.update(progress_task, completed=completed_tasks)
|
377
|
-
|
378
317
|
layout["details"].update(self.show_task_status(next_task))
|
379
|
-
|
380
318
|
layout["tasks"].update(self.show_task_table())
|
381
|
-
|
382
319
|
successful = sum(
|
383
|
-
|
384
|
-
|
385
|
-
|
320
|
+
(
|
321
|
+
1
|
322
|
+
for task in self.workflow.tasks.values()
|
323
|
+
if task.status == TaskStatus.SUCCESS
|
324
|
+
)
|
386
325
|
)
|
387
326
|
failed = sum(
|
388
|
-
|
389
|
-
|
390
|
-
|
327
|
+
(
|
328
|
+
1
|
329
|
+
for task in self.workflow.tasks.values()
|
330
|
+
if task.status == TaskStatus.FAILED
|
331
|
+
)
|
391
332
|
)
|
392
333
|
skipped = sum(
|
393
|
-
|
394
|
-
|
395
|
-
|
334
|
+
(
|
335
|
+
1
|
336
|
+
for task in self.workflow.tasks.values()
|
337
|
+
if task.status == TaskStatus.SKIPPED
|
338
|
+
)
|
396
339
|
)
|
397
|
-
|
398
340
|
summary = Panel(
|
399
|
-
f"Workflow completed!\n\n"
|
400
|
-
f"[green]✅ Successful tasks: {successful}[/green]\n"
|
401
|
-
f"[red]❌ Failed tasks: {failed}[/red]\n"
|
402
|
-
f"[blue]⏩ Skipped tasks: {skipped}[/blue]",
|
341
|
+
f"Workflow completed!\n\n[green]✅ Successful tasks: {successful}[/green]\n[red]❌ Failed tasks: {failed}[/red]\n[blue]⏩ Skipped tasks: {skipped}[/blue]",
|
403
342
|
title="Summary",
|
404
343
|
border_style="cyan",
|
405
344
|
)
|
406
345
|
layout["details"].update(summary)
|
407
|
-
|
408
346
|
except KeyboardInterrupt:
|
409
347
|
layout["footer"].update(
|
410
|
-
Panel(
|
411
|
-
"Interrupted by user",
|
412
|
-
style="yellow",
|
413
|
-
box=ROUNDED,
|
414
|
-
)
|
348
|
+
Panel("Interrupted by user", style="yellow", box=ROUNDED)
|
415
349
|
)
|
416
|
-
|
417
350
|
self.console.print("\nWorkflow Status:")
|
418
351
|
self.workflow.display_task_tree()
|
419
352
|
|
@@ -421,53 +354,38 @@ class InteractiveCLI:
|
|
421
354
|
self, prompt: str, directory: Path, default: str | None = None
|
422
355
|
) -> Path:
|
423
356
|
self.console.print(f"\n[bold]{prompt}[/bold]")
|
424
|
-
|
425
357
|
files = list(directory.iterdir())
|
426
358
|
files.sort()
|
427
|
-
|
428
359
|
table = Table(title=f"Files in {directory}", box=ROUNDED)
|
429
360
|
table.add_column("#", style="cyan")
|
430
361
|
table.add_column("Filename", style="green")
|
431
362
|
table.add_column("Size", style="blue")
|
432
363
|
table.add_column("Modified", style="yellow")
|
433
|
-
|
434
364
|
for i, file in enumerate(files, 1):
|
435
365
|
if file.is_file():
|
436
366
|
size = f"{file.stat().st_size / 1024:.1f} KB"
|
437
367
|
mtime = time.strftime(
|
438
|
-
"%Y-%m-%d %H:%M:%S",
|
439
|
-
time.localtime(file.stat().st_mtime),
|
368
|
+
"%Y-%m-%d %H:%M:%S", time.localtime(file.stat().st_mtime)
|
440
369
|
)
|
441
370
|
table.add_row(str(i), file.name, size, mtime)
|
442
|
-
|
443
371
|
self.console.print(table)
|
444
|
-
|
445
|
-
selection = Prompt.ask(
|
446
|
-
"Enter file number or name",
|
447
|
-
default=default or "",
|
448
|
-
)
|
449
|
-
|
372
|
+
selection = Prompt.ask("Enter file number or name", default=default or "")
|
450
373
|
if selection.isdigit() and 1 <= int(selection) <= len(files):
|
451
374
|
return files[int(selection) - 1]
|
452
375
|
else:
|
453
376
|
for file in files:
|
454
377
|
if file.name == selection:
|
455
378
|
return file
|
456
|
-
|
457
379
|
return directory / selection
|
458
380
|
|
459
381
|
def confirm_dangerous_action(self, action: str, details: str) -> bool:
|
460
382
|
panel = Panel(
|
461
|
-
f"[bold red]WARNING: {action}[/bold red]\n\n{details}\n\
|
462
|
-
"This action cannot be undone. Please type the action name to confirm.",
|
383
|
+
f"[bold red]WARNING: {action}[/bold red]\n\n{details}\n\nThis action cannot be undone. Please type the action name to confirm.",
|
463
384
|
title="Confirmation Required",
|
464
385
|
border_style="red",
|
465
386
|
)
|
466
|
-
|
467
387
|
self.console.print(panel)
|
468
|
-
|
469
388
|
confirmation = Prompt.ask("Type the action name to confirm")
|
470
|
-
|
471
389
|
return confirmation.lower() == action.lower()
|
472
390
|
|
473
391
|
def show_error(self, error: CrackerjackError, verbose: bool = False) -> None:
|
@@ -477,7 +395,6 @@ class InteractiveCLI:
|
|
477
395
|
def launch_interactive_cli(version: str) -> None:
|
478
396
|
console = Console()
|
479
397
|
cli = InteractiveCLI(console)
|
480
|
-
|
481
398
|
cli.show_banner(version)
|
482
399
|
cli.create_standard_workflow()
|
483
400
|
cli.run_interactive()
|
crackerjack/py313.py
CHANGED
@@ -1,13 +1,3 @@
|
|
1
|
-
"""Python 3.13+ specific features and enhancements.
|
2
|
-
|
3
|
-
This module contains implementations that leverage the latest Python 3.13+ features.
|
4
|
-
It serves as a showcase for modern Python capabilities including:
|
5
|
-
- Pattern matching
|
6
|
-
- PEP 695 type parameter syntax
|
7
|
-
- Self type annotations
|
8
|
-
- More precise type hints
|
9
|
-
"""
|
10
|
-
|
11
1
|
import subprocess
|
12
2
|
import typing
|
13
3
|
from enum import Enum, auto
|
@@ -31,21 +21,16 @@ class CommandResult(TypedDict):
|
|
31
21
|
def process_command_output(result: CommandResult) -> tuple[bool, str]:
|
32
22
|
match result:
|
33
23
|
case {"success": True, "stdout": stdout} if stdout.strip():
|
34
|
-
return True, stdout
|
35
|
-
|
24
|
+
return (True, stdout)
|
36
25
|
case {"success": True}:
|
37
|
-
return True, "Command completed successfully with no output"
|
38
|
-
|
26
|
+
return (True, "Command completed successfully with no output")
|
39
27
|
case {"success": False, "exit_code": code, "stderr": stderr} if code == 127:
|
40
|
-
return False, f"Command not found: {stderr}"
|
41
|
-
|
28
|
+
return (False, f"Command not found: {stderr}")
|
42
29
|
case {"success": False, "exit_code": code} if code > 0:
|
43
|
-
return False, f"Command failed with exit code {code}: {result['stderr']}"
|
44
|
-
|
30
|
+
return (False, f"Command failed with exit code {code}: {result['stderr']}")
|
45
31
|
case _:
|
46
32
|
pass
|
47
|
-
|
48
|
-
return False, "Unknown command result pattern"
|
33
|
+
return (False, "Unknown command result pattern")
|
49
34
|
|
50
35
|
|
51
36
|
class HookStatus(Enum):
|
@@ -66,24 +51,18 @@ def analyze_hook_result(result: HookResult) -> str:
|
|
66
51
|
match result:
|
67
52
|
case {"status": HookStatus.SUCCESS, "hook_id": hook_id}:
|
68
53
|
return f"✅ Hook {hook_id} passed successfully"
|
69
|
-
|
70
54
|
case {"status": HookStatus.FAILURE, "hook_id": hook_id, "output": output} if (
|
71
55
|
"fixable" in output
|
72
56
|
):
|
73
57
|
return f"🔧 Hook {hook_id} failed with fixable issues"
|
74
|
-
|
75
58
|
case {"status": HookStatus.FAILURE, "hook_id": hook_id}:
|
76
59
|
return f"❌ Hook {hook_id} failed"
|
77
|
-
|
78
60
|
case {"status": HookStatus.SKIPPED, "hook_id": hook_id}:
|
79
61
|
return f"⏩ Hook {hook_id} was skipped"
|
80
|
-
|
81
62
|
case {"status": HookStatus.ERROR, "hook_id": hook_id, "output": output}:
|
82
63
|
return f"💥 Hook {hook_id} encountered an error: {output}"
|
83
|
-
|
84
64
|
case _:
|
85
65
|
pass
|
86
|
-
|
87
66
|
return "Unknown hook result pattern"
|
88
67
|
|
89
68
|
|
@@ -106,29 +85,22 @@ class ModernConfigManager:
|
|
106
85
|
def categorize_file(file_path: Path) -> str:
|
107
86
|
path_str = str(file_path)
|
108
87
|
name = file_path
|
109
|
-
|
110
88
|
match path_str:
|
111
89
|
case s if name.suffix == ".py" and "/tests/" in s:
|
112
90
|
return "Python Test File"
|
113
|
-
|
114
91
|
case s if name.suffix == ".py" and "__init__.py" in name.name:
|
115
92
|
return "Python Module Init"
|
116
|
-
|
117
93
|
case s if name.suffix == ".py":
|
118
94
|
return "Python Source File"
|
119
|
-
|
120
95
|
case s if name.suffix in {".md", ".rst", ".txt"}:
|
121
96
|
return "Documentation File"
|
122
|
-
|
123
97
|
case s if name.stem.startswith(".") or name.name in {
|
124
98
|
".gitignore",
|
125
99
|
".pre-commit-config.yaml",
|
126
100
|
}:
|
127
101
|
return "Configuration File"
|
128
|
-
|
129
102
|
case _:
|
130
103
|
pass
|
131
|
-
|
132
104
|
return "Unknown File Type"
|
133
105
|
|
134
106
|
|
@@ -138,13 +110,11 @@ def process_hook_results[T, R](
|
|
138
110
|
failure_handler: typing.Callable[[T], R],
|
139
111
|
) -> list[R]:
|
140
112
|
processed_results = []
|
141
|
-
|
142
113
|
for result in results:
|
143
114
|
if isinstance(result, dict) and result.get("status") == HookStatus.SUCCESS:
|
144
115
|
processed_results.append(success_handler(result))
|
145
116
|
else:
|
146
117
|
processed_results.append(failure_handler(result))
|
147
|
-
|
148
118
|
return processed_results
|
149
119
|
|
150
120
|
|
@@ -156,26 +126,21 @@ class EnhancedCommandRunner:
|
|
156
126
|
import time
|
157
127
|
|
158
128
|
start_time = time.time()
|
159
|
-
|
160
129
|
try:
|
161
130
|
process = subprocess.run(
|
162
131
|
cmd, capture_output=True, text=True, cwd=self.working_dir, **kwargs
|
163
132
|
)
|
164
|
-
|
165
133
|
duration_ms = (time.time() - start_time) * 1000
|
166
|
-
|
167
134
|
return CommandResult(
|
168
|
-
success=
|
135
|
+
success=process.returncode == 0,
|
169
136
|
exit_code=process.returncode,
|
170
137
|
stdout=process.stdout,
|
171
138
|
stderr=process.stderr,
|
172
139
|
command=cmd,
|
173
140
|
duration_ms=duration_ms,
|
174
141
|
)
|
175
|
-
|
176
142
|
except subprocess.SubprocessError as e:
|
177
143
|
duration_ms = (time.time() - start_time) * 1000
|
178
|
-
|
179
144
|
return CommandResult(
|
180
145
|
success=False,
|
181
146
|
exit_code=-1,
|
@@ -192,30 +157,25 @@ class EnhancedCommandRunner:
|
|
192
157
|
def clean_python_code(code: str) -> str:
|
193
158
|
lines = code.splitlines()
|
194
159
|
cleaned_lines = []
|
195
|
-
|
196
160
|
for line in lines:
|
197
161
|
match line.strip():
|
198
162
|
case "":
|
199
163
|
if not cleaned_lines or cleaned_lines[-1].strip():
|
200
164
|
cleaned_lines.append("")
|
201
|
-
|
202
165
|
case s if s.startswith(("import ", "from ")):
|
203
166
|
cleaned_lines.append(line)
|
204
|
-
|
205
167
|
case s if s.startswith("#"):
|
206
168
|
continue
|
207
|
-
|
208
|
-
|
209
|
-
|
169
|
+
case s if "#" in s and (
|
170
|
+
not any(
|
171
|
+
(skip in s for skip in ("# noqa", "# type:", "# pragma", "# skip"))
|
172
|
+
)
|
210
173
|
):
|
211
174
|
code_part = line.split("#", 1)[0].rstrip()
|
212
175
|
if code_part:
|
213
176
|
cleaned_lines.append(code_part)
|
214
|
-
|
215
177
|
case s if s.startswith('"""') or s.startswith("'''"):
|
216
178
|
continue
|
217
|
-
|
218
179
|
case _:
|
219
180
|
cleaned_lines.append(line)
|
220
|
-
|
221
181
|
return "\n".join(cleaned_lines)
|
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.9"
|
8
8
|
description = "Crackerjack: code quality toolkit"
|
9
9
|
readme = "README.md"
|
10
10
|
keywords = [
|
@@ -42,6 +42,7 @@ classifiers = [
|
|
42
42
|
]
|
43
43
|
dependencies = [
|
44
44
|
"autotyping>=24.9",
|
45
|
+
"keyring>=25.6",
|
45
46
|
"pdm>=2.24.2",
|
46
47
|
"pdm-bump>=0.9.12",
|
47
48
|
"pre-commit>=4.2",
|
@@ -216,6 +217,7 @@ exclude-deps = [
|
|
216
217
|
"tomli-w",
|
217
218
|
"google-crc32c",
|
218
219
|
"pytest-timeout",
|
220
|
+
"keyring",
|
219
221
|
]
|
220
222
|
|
221
223
|
[tool.refurb]
|
@@ -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
|