crackerjack 0.19.8__py3-none-any.whl → 0.20.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/.gitignore +2 -0
- crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
- crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
- crackerjack/__init__.py +38 -1
- crackerjack/__main__.py +23 -3
- crackerjack/crackerjack.py +295 -40
- crackerjack/errors.py +176 -0
- crackerjack/interactive.py +487 -0
- crackerjack/py313.py +221 -0
- crackerjack/pyproject.toml +106 -106
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.0.dist-info}/METADATA +95 -3
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.0.dist-info}/RECORD +15 -11
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.0.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.19.8.dist-info → crackerjack-0.20.0.dist-info}/licenses/LICENSE +0 -0
crackerjack/.gitignore
CHANGED
Binary file
|
Binary file
|
crackerjack/__init__.py
CHANGED
@@ -1,5 +1,42 @@
|
|
1
1
|
import typing as t
|
2
2
|
|
3
3
|
from .crackerjack import Crackerjack, create_crackerjack_runner
|
4
|
+
from .errors import (
|
5
|
+
CleaningError,
|
6
|
+
ConfigError,
|
7
|
+
CrackerjackError,
|
8
|
+
ErrorCode,
|
9
|
+
ExecutionError,
|
10
|
+
FileError,
|
11
|
+
GitError,
|
12
|
+
PublishError,
|
13
|
+
TestError,
|
14
|
+
check_command_result,
|
15
|
+
check_file_exists,
|
16
|
+
handle_error,
|
17
|
+
)
|
4
18
|
|
5
|
-
|
19
|
+
try:
|
20
|
+
from importlib.metadata import version
|
21
|
+
|
22
|
+
__version__ = version("crackerjack")
|
23
|
+
except (ImportError, ModuleNotFoundError):
|
24
|
+
__version__ = "0.19.8"
|
25
|
+
|
26
|
+
__all__: t.Sequence[str] = [
|
27
|
+
"create_crackerjack_runner",
|
28
|
+
"Crackerjack",
|
29
|
+
"__version__",
|
30
|
+
"CrackerjackError",
|
31
|
+
"ConfigError",
|
32
|
+
"ExecutionError",
|
33
|
+
"TestError",
|
34
|
+
"PublishError",
|
35
|
+
"GitError",
|
36
|
+
"FileError",
|
37
|
+
"CleaningError",
|
38
|
+
"ErrorCode",
|
39
|
+
"handle_error",
|
40
|
+
"check_file_exists",
|
41
|
+
"check_command_result",
|
42
|
+
]
|
crackerjack/__main__.py
CHANGED
@@ -38,6 +38,7 @@ class Options(BaseModel):
|
|
38
38
|
ai_agent: bool = False
|
39
39
|
create_pr: bool = False
|
40
40
|
skip_hooks: bool = False
|
41
|
+
rich_ui: bool = False
|
41
42
|
|
42
43
|
@classmethod
|
43
44
|
@field_validator("publish", "bump", mode="before")
|
@@ -84,7 +85,7 @@ cli_options = {
|
|
84
85
|
False,
|
85
86
|
"-x",
|
86
87
|
"--clean",
|
87
|
-
help="Remove docstrings, line comments, and unnecessary whitespace.",
|
88
|
+
help="Remove docstrings, line comments, and unnecessary whitespace from source code (doesn't affect test files).",
|
88
89
|
),
|
89
90
|
"test": typer.Option(False, "-t", "--test", help="Run tests."),
|
90
91
|
"benchmark": typer.Option(
|
@@ -121,6 +122,11 @@ cli_options = {
|
|
121
122
|
"--pr",
|
122
123
|
help="Create a pull request to the upstream repository.",
|
123
124
|
),
|
125
|
+
"rich_ui": typer.Option(
|
126
|
+
False,
|
127
|
+
"--rich-ui",
|
128
|
+
help="Use the interactive Rich UI for a better experience.",
|
129
|
+
),
|
124
130
|
"ai_agent": typer.Option(
|
125
131
|
False,
|
126
132
|
"--ai-agent",
|
@@ -150,6 +156,7 @@ def main(
|
|
150
156
|
],
|
151
157
|
skip_hooks: bool = cli_options["skip_hooks"],
|
152
158
|
create_pr: bool = cli_options["create_pr"],
|
159
|
+
rich_ui: bool = cli_options["rich_ui"],
|
153
160
|
ai_agent: bool = cli_options["ai_agent"],
|
154
161
|
) -> None:
|
155
162
|
options = Options(
|
@@ -170,6 +177,7 @@ def main(
|
|
170
177
|
all=all,
|
171
178
|
ai_agent=ai_agent,
|
172
179
|
create_pr=create_pr,
|
180
|
+
rich_ui=rich_ui,
|
173
181
|
)
|
174
182
|
|
175
183
|
if ai_agent:
|
@@ -177,8 +185,20 @@ def main(
|
|
177
185
|
|
178
186
|
os.environ["AI_AGENT"] = "1"
|
179
187
|
|
180
|
-
|
181
|
-
|
188
|
+
if rich_ui:
|
189
|
+
from crackerjack.interactive import launch_interactive_cli
|
190
|
+
|
191
|
+
try:
|
192
|
+
from importlib.metadata import version
|
193
|
+
|
194
|
+
pkg_version = version("crackerjack")
|
195
|
+
except (ImportError, ModuleNotFoundError):
|
196
|
+
pkg_version = "0.19.8"
|
197
|
+
|
198
|
+
launch_interactive_cli(pkg_version)
|
199
|
+
else:
|
200
|
+
runner = create_crackerjack_runner(console=console)
|
201
|
+
runner.process(options)
|
182
202
|
|
183
203
|
|
184
204
|
if __name__ == "__main__":
|
crackerjack/crackerjack.py
CHANGED
@@ -62,22 +62,77 @@ class CodeCleaner:
|
|
62
62
|
self.clean_file(file_path)
|
63
63
|
|
64
64
|
def clean_file(self, file_path: Path) -> None:
|
65
|
+
from .errors import CleaningError, ErrorCode, FileError, handle_error
|
66
|
+
|
65
67
|
try:
|
66
68
|
if file_path.resolve() == Path(__file__).resolve():
|
67
69
|
self.console.print(f"Skipping cleaning of {file_path} (self file).")
|
68
70
|
return
|
69
71
|
except Exception as e:
|
70
|
-
|
72
|
+
error = FileError(
|
73
|
+
message="Error comparing file paths",
|
74
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
75
|
+
details=f"Failed to compare {file_path} with the current file: {e}",
|
76
|
+
recovery="This is likely a file system permission issue. Check file permissions.",
|
77
|
+
exit_code=0, # Non-fatal error
|
78
|
+
)
|
79
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
80
|
+
return
|
81
|
+
|
71
82
|
try:
|
72
|
-
|
83
|
+
# Check if file exists and is readable
|
84
|
+
if not file_path.exists():
|
85
|
+
error = FileError(
|
86
|
+
message="File not found",
|
87
|
+
error_code=ErrorCode.FILE_NOT_FOUND,
|
88
|
+
details=f"The file {file_path} does not exist.",
|
89
|
+
recovery="Check the file path and ensure the file exists.",
|
90
|
+
exit_code=0, # Non-fatal error
|
91
|
+
)
|
92
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
93
|
+
return
|
94
|
+
|
95
|
+
try:
|
96
|
+
code = file_path.read_text()
|
97
|
+
except Exception as e:
|
98
|
+
error = FileError(
|
99
|
+
message="Error reading file",
|
100
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
101
|
+
details=f"Failed to read {file_path}: {e}",
|
102
|
+
recovery="Check file permissions and ensure the file is not locked by another process.",
|
103
|
+
exit_code=0, # Non-fatal error
|
104
|
+
)
|
105
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
106
|
+
return
|
107
|
+
|
108
|
+
# Process the file content
|
73
109
|
code = self.remove_docstrings(code)
|
74
110
|
code = self.remove_line_comments(code)
|
75
111
|
code = self.remove_extra_whitespace(code)
|
76
112
|
code = self.reformat_code(code)
|
77
|
-
|
78
|
-
|
113
|
+
|
114
|
+
try:
|
115
|
+
file_path.write_text(code) # type: ignore
|
116
|
+
self.console.print(f"Cleaned: {file_path}")
|
117
|
+
except Exception as e:
|
118
|
+
error = FileError(
|
119
|
+
message="Error writing file",
|
120
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
121
|
+
details=f"Failed to write to {file_path}: {e}",
|
122
|
+
recovery="Check file permissions and ensure the file is not locked by another process.",
|
123
|
+
exit_code=0, # Non-fatal error
|
124
|
+
)
|
125
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
126
|
+
|
79
127
|
except Exception as e:
|
80
|
-
|
128
|
+
error = CleaningError(
|
129
|
+
message="Error cleaning file",
|
130
|
+
error_code=ErrorCode.CODE_CLEANING_ERROR,
|
131
|
+
details=f"Failed to clean {file_path}: {e}",
|
132
|
+
recovery="This could be due to syntax errors in the file. Try manually checking the file for syntax errors.",
|
133
|
+
exit_code=0, # Non-fatal error
|
134
|
+
)
|
135
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
81
136
|
|
82
137
|
def remove_line_comments(self, code: str) -> str:
|
83
138
|
new_lines = []
|
@@ -199,14 +254,30 @@ class CodeCleaner:
|
|
199
254
|
return "\n".join(cleaned_lines)
|
200
255
|
|
201
256
|
def reformat_code(self, code: str) -> str | None:
|
257
|
+
from .errors import CleaningError, ErrorCode, handle_error
|
258
|
+
|
202
259
|
try:
|
203
260
|
import tempfile
|
204
261
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
262
|
+
# Create a temporary file for formatting
|
263
|
+
try:
|
264
|
+
with tempfile.NamedTemporaryFile(
|
265
|
+
suffix=".py", mode="w+", delete=False
|
266
|
+
) as temp:
|
267
|
+
temp_path = Path(temp.name)
|
268
|
+
temp_path.write_text(code)
|
269
|
+
except Exception as e:
|
270
|
+
error = CleaningError(
|
271
|
+
message="Failed to create temporary file for formatting",
|
272
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
273
|
+
details=f"Error: {e}",
|
274
|
+
recovery="Check disk space and permissions for the temp directory.",
|
275
|
+
exit_code=0, # Non-fatal
|
276
|
+
)
|
277
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
278
|
+
return code
|
279
|
+
|
280
|
+
# Run Ruff to format the code
|
210
281
|
try:
|
211
282
|
result = subprocess.run(
|
212
283
|
["ruff", "format", str(temp_path)],
|
@@ -214,20 +285,58 @@ class CodeCleaner:
|
|
214
285
|
capture_output=True,
|
215
286
|
text=True,
|
216
287
|
)
|
288
|
+
|
217
289
|
if result.returncode == 0:
|
218
|
-
|
290
|
+
try:
|
291
|
+
formatted_code = temp_path.read_text()
|
292
|
+
except Exception as e:
|
293
|
+
error = CleaningError(
|
294
|
+
message="Failed to read formatted code",
|
295
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
296
|
+
details=f"Error reading temporary file after formatting: {e}",
|
297
|
+
recovery="This might be a permissions issue. Check if Ruff is installed properly.",
|
298
|
+
exit_code=0, # Non-fatal
|
299
|
+
)
|
300
|
+
handle_error(
|
301
|
+
error, self.console, verbose=True, exit_on_error=False
|
302
|
+
)
|
303
|
+
formatted_code = code
|
219
304
|
else:
|
220
|
-
|
305
|
+
error = CleaningError(
|
306
|
+
message="Ruff formatting failed",
|
307
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
308
|
+
details=f"Ruff output: {result.stderr}",
|
309
|
+
recovery="The file might contain syntax errors. Check the file manually.",
|
310
|
+
exit_code=0, # Non-fatal
|
311
|
+
)
|
312
|
+
handle_error(error, self.console, exit_on_error=False)
|
221
313
|
formatted_code = code
|
222
314
|
except Exception as e:
|
223
|
-
|
315
|
+
error = CleaningError(
|
316
|
+
message="Error running Ruff formatter",
|
317
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
318
|
+
details=f"Error: {e}",
|
319
|
+
recovery="Ensure Ruff is installed correctly. Run 'pip install ruff' to install it.",
|
320
|
+
exit_code=0, # Non-fatal
|
321
|
+
)
|
322
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
224
323
|
formatted_code = code
|
225
324
|
finally:
|
325
|
+
# Clean up temporary file
|
226
326
|
with suppress(FileNotFoundError):
|
227
327
|
temp_path.unlink()
|
328
|
+
|
228
329
|
return formatted_code
|
330
|
+
|
229
331
|
except Exception as e:
|
230
|
-
|
332
|
+
error = CleaningError(
|
333
|
+
message="Unexpected error during code formatting",
|
334
|
+
error_code=ErrorCode.FORMATTING_ERROR,
|
335
|
+
details=f"Error: {e}",
|
336
|
+
recovery="This is an unexpected error. Please report this issue.",
|
337
|
+
exit_code=0, # Non-fatal
|
338
|
+
)
|
339
|
+
handle_error(error, self.console, verbose=True, exit_on_error=False)
|
231
340
|
return code
|
232
341
|
|
233
342
|
|
@@ -368,18 +477,52 @@ class ProjectManager:
|
|
368
477
|
dry_run: bool = False
|
369
478
|
|
370
479
|
def run_interactive(self, hook: str) -> None:
|
480
|
+
from .errors import ErrorCode, ExecutionError, handle_error
|
481
|
+
|
371
482
|
success: bool = False
|
372
|
-
|
373
|
-
|
483
|
+
attempts = 0
|
484
|
+
max_attempts = 3
|
485
|
+
|
486
|
+
while not success and attempts < max_attempts:
|
487
|
+
attempts += 1
|
488
|
+
result = self.execute_command(
|
374
489
|
["pre-commit", "run", hook.lower(), "--all-files"]
|
375
490
|
)
|
376
|
-
|
377
|
-
|
491
|
+
|
492
|
+
if result.returncode > 0:
|
493
|
+
self.console.print(
|
494
|
+
f"\n\n[yellow]Hook '{hook}' failed (attempt {attempts}/{max_attempts})[/yellow]"
|
495
|
+
)
|
496
|
+
|
497
|
+
# Give more detailed information about the failure
|
498
|
+
if result.stderr:
|
499
|
+
self.console.print(f"[red]Error details:[/red]\n{result.stderr}")
|
500
|
+
|
501
|
+
retry = input(f"Retry running {hook.title()}? (y/N): ")
|
378
502
|
self.console.print()
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
503
|
+
|
504
|
+
if retry.strip().lower() != "y":
|
505
|
+
error = ExecutionError(
|
506
|
+
message=f"Interactive hook '{hook}' failed",
|
507
|
+
error_code=ErrorCode.PRE_COMMIT_ERROR,
|
508
|
+
details=f"Hook execution output:\n{result.stderr or result.stdout}",
|
509
|
+
recovery=f"Try running the hook manually: pre-commit run {hook.lower()} --all-files",
|
510
|
+
exit_code=1,
|
511
|
+
)
|
512
|
+
handle_error(error=error, console=self.console)
|
513
|
+
else:
|
514
|
+
self.console.print(f"[green]✅ Hook '{hook}' succeeded![/green]")
|
515
|
+
success = True
|
516
|
+
|
517
|
+
if not success:
|
518
|
+
error = ExecutionError(
|
519
|
+
message=f"Interactive hook '{hook}' failed after {max_attempts} attempts",
|
520
|
+
error_code=ErrorCode.PRE_COMMIT_ERROR,
|
521
|
+
details="The hook continued to fail after multiple attempts.",
|
522
|
+
recovery=f"Fix the issues manually and run: pre-commit run {hook.lower()} --all-files",
|
523
|
+
exit_code=1,
|
524
|
+
)
|
525
|
+
handle_error(error=error, console=self.console)
|
383
526
|
|
384
527
|
def update_pkg_configs(self) -> None:
|
385
528
|
self.config_manager.copy_configs()
|
@@ -399,13 +542,25 @@ class ProjectManager:
|
|
399
542
|
self.config_manager.update_pyproject_configs()
|
400
543
|
|
401
544
|
def run_pre_commit(self) -> None:
|
545
|
+
from .errors import ErrorCode, ExecutionError, handle_error
|
546
|
+
|
402
547
|
self.console.print("\nRunning pre-commit hooks...\n")
|
403
548
|
check_all = self.execute_command(["pre-commit", "run", "--all-files"])
|
549
|
+
|
404
550
|
if check_all.returncode > 0:
|
551
|
+
# First retry
|
552
|
+
self.console.print("\nSome pre-commit hooks failed. Retrying once...\n")
|
405
553
|
check_all = self.execute_command(["pre-commit", "run", "--all-files"])
|
554
|
+
|
406
555
|
if check_all.returncode > 0:
|
407
|
-
|
408
|
-
|
556
|
+
error = ExecutionError(
|
557
|
+
message="Pre-commit hooks failed",
|
558
|
+
error_code=ErrorCode.PRE_COMMIT_ERROR,
|
559
|
+
details="Pre-commit hooks failed even after a retry. Check the output above for specific hook failures.",
|
560
|
+
recovery="Review the error messages above. Manually fix the issues or run specific hooks interactively with 'pre-commit run <hook-id>'.",
|
561
|
+
exit_code=1,
|
562
|
+
)
|
563
|
+
handle_error(error=error, console=self.console, verbose=True)
|
409
564
|
|
410
565
|
def execute_command(
|
411
566
|
self, cmd: list[str], **kwargs: t.Any
|
@@ -463,6 +618,8 @@ class Crackerjack:
|
|
463
618
|
self.project_manager.pkg_dir = self.pkg_dir
|
464
619
|
|
465
620
|
def _update_project(self, options: OptionsProtocol) -> None:
|
621
|
+
from .errors import ErrorCode, ExecutionError, handle_error
|
622
|
+
|
466
623
|
if not options.no_config_updates:
|
467
624
|
self.project_manager.update_pkg_configs()
|
468
625
|
result: CompletedProcess[str] = self.execute_command(
|
@@ -471,8 +628,21 @@ class Crackerjack:
|
|
471
628
|
if result.returncode == 0:
|
472
629
|
self.console.print("PDM installed: ✅\n")
|
473
630
|
else:
|
474
|
-
|
475
|
-
"
|
631
|
+
error = ExecutionError(
|
632
|
+
message="PDM installation failed",
|
633
|
+
error_code=ErrorCode.PDM_INSTALL_ERROR,
|
634
|
+
details=f"Command output:\n{result.stderr}",
|
635
|
+
recovery="Ensure PDM is installed. Run `pipx install pdm` and try again. Check for network issues or package conflicts.",
|
636
|
+
exit_code=1,
|
637
|
+
)
|
638
|
+
|
639
|
+
# Don't exit immediately - this isn't always fatal
|
640
|
+
handle_error(
|
641
|
+
error=error,
|
642
|
+
console=self.console,
|
643
|
+
verbose=options.verbose,
|
644
|
+
ai_agent=options.ai_agent,
|
645
|
+
exit_on_error=False,
|
476
646
|
)
|
477
647
|
|
478
648
|
def _update_precommit(self, options: OptionsProtocol) -> None:
|
@@ -488,10 +658,8 @@ class Crackerjack:
|
|
488
658
|
if options.clean:
|
489
659
|
if self.pkg_dir:
|
490
660
|
self.code_cleaner.clean_files(self.pkg_dir)
|
491
|
-
|
492
|
-
|
493
|
-
self.console.print("\nCleaning tests directory...\n")
|
494
|
-
self.code_cleaner.clean_files(tests_dir)
|
661
|
+
# Skip cleaning test files as they may contain test data in docstrings and comments
|
662
|
+
# that are necessary for the tests to function properly
|
495
663
|
|
496
664
|
def _prepare_pytest_command(self, options: OptionsProtocol) -> list[str]:
|
497
665
|
"""Prepare pytest command with appropriate options.
|
@@ -557,6 +725,8 @@ class Crackerjack:
|
|
557
725
|
def _run_pytest_process(
|
558
726
|
self, test_command: list[str]
|
559
727
|
) -> subprocess.CompletedProcess[str]:
|
728
|
+
from .errors import ErrorCode, ExecutionError, handle_error
|
729
|
+
|
560
730
|
try:
|
561
731
|
process = subprocess.Popen(
|
562
732
|
test_command,
|
@@ -572,6 +742,13 @@ class Crackerjack:
|
|
572
742
|
stderr_data = []
|
573
743
|
while process.poll() is None:
|
574
744
|
if time.time() - start_time > timeout:
|
745
|
+
error = ExecutionError(
|
746
|
+
message="Test execution timed out after 5 minutes.",
|
747
|
+
error_code=ErrorCode.COMMAND_TIMEOUT,
|
748
|
+
details=f"Command: {' '.join(test_command)}\nTimeout: {timeout} seconds",
|
749
|
+
recovery="Check for infinite loops or deadlocks in your tests. Consider increasing the timeout or optimizing your tests.",
|
750
|
+
)
|
751
|
+
|
575
752
|
self.console.print(
|
576
753
|
"[red]Test execution timed out after 5 minutes. Terminating...[/red]"
|
577
754
|
)
|
@@ -580,7 +757,11 @@ class Crackerjack:
|
|
580
757
|
process.wait(timeout=5)
|
581
758
|
except subprocess.TimeoutExpired:
|
582
759
|
process.kill()
|
760
|
+
stderr_data.append(
|
761
|
+
"Process had to be forcefully terminated after timeout."
|
762
|
+
)
|
583
763
|
break
|
764
|
+
|
584
765
|
if process.stdout:
|
585
766
|
line = process.stdout.readline()
|
586
767
|
if line:
|
@@ -592,6 +773,7 @@ class Crackerjack:
|
|
592
773
|
stderr_data.append(line)
|
593
774
|
self.console.print(f"[red]{line}[/red]", end="")
|
594
775
|
time.sleep(0.1)
|
776
|
+
|
595
777
|
if process.stdout:
|
596
778
|
for line in process.stdout:
|
597
779
|
stdout_data.append(line)
|
@@ -600,23 +782,42 @@ class Crackerjack:
|
|
600
782
|
for line in process.stderr:
|
601
783
|
stderr_data.append(line)
|
602
784
|
self.console.print(f"[red]{line}[/red]", end="")
|
785
|
+
|
603
786
|
returncode = process.returncode or 0
|
604
787
|
stdout = "".join(stdout_data)
|
605
788
|
stderr = "".join(stderr_data)
|
789
|
+
|
606
790
|
return subprocess.CompletedProcess(
|
607
791
|
args=test_command, returncode=returncode, stdout=stdout, stderr=stderr
|
608
792
|
)
|
609
793
|
|
610
794
|
except Exception as e:
|
611
|
-
|
795
|
+
error = ExecutionError(
|
796
|
+
message=f"Error running tests: {e}",
|
797
|
+
error_code=ErrorCode.TEST_EXECUTION_ERROR,
|
798
|
+
details=f"Command: {' '.join(test_command)}\nError: {e}",
|
799
|
+
recovery="Check if pytest is installed and that your test files are properly formatted.",
|
800
|
+
exit_code=1,
|
801
|
+
)
|
802
|
+
|
803
|
+
# Don't exit here, let the caller handle it
|
804
|
+
handle_error(
|
805
|
+
error=error, console=self.console, verbose=True, exit_on_error=False
|
806
|
+
)
|
807
|
+
|
612
808
|
return subprocess.CompletedProcess(test_command, 1, "", str(e))
|
613
809
|
|
614
810
|
def _report_test_results(
|
615
811
|
self, result: subprocess.CompletedProcess[str], ai_agent: str
|
616
812
|
) -> None:
|
813
|
+
from .errors import ErrorCode, TestError, handle_error
|
814
|
+
|
617
815
|
if result.returncode > 0:
|
816
|
+
error_details = None
|
618
817
|
if result.stderr:
|
619
818
|
self.console.print(result.stderr)
|
819
|
+
error_details = result.stderr
|
820
|
+
|
620
821
|
if ai_agent:
|
621
822
|
self.console.print(
|
622
823
|
'[json]{"status": "failed", "action": "tests", "returncode": '
|
@@ -624,8 +825,19 @@ class Crackerjack:
|
|
624
825
|
+ "}[/json]"
|
625
826
|
)
|
626
827
|
else:
|
627
|
-
|
628
|
-
|
828
|
+
# Use the structured error handler
|
829
|
+
error = TestError(
|
830
|
+
message="Tests failed. Please fix the errors.",
|
831
|
+
error_code=ErrorCode.TEST_FAILURE,
|
832
|
+
details=error_details,
|
833
|
+
recovery="Review the test output above for specific failures. Fix the issues in your code and run tests again.",
|
834
|
+
exit_code=1,
|
835
|
+
)
|
836
|
+
handle_error(
|
837
|
+
error=error,
|
838
|
+
console=self.console,
|
839
|
+
ai_agent=(ai_agent != ""),
|
840
|
+
)
|
629
841
|
|
630
842
|
if ai_agent:
|
631
843
|
self.console.print('[json]{"status": "success", "action": "tests"}[/json]')
|
@@ -653,25 +865,68 @@ class Crackerjack:
|
|
653
865
|
break
|
654
866
|
|
655
867
|
def _publish_project(self, options: OptionsProtocol) -> None:
|
868
|
+
from .errors import ErrorCode, PublishError, handle_error
|
869
|
+
|
656
870
|
if options.publish:
|
657
871
|
if platform.system() == "Darwin":
|
658
872
|
authorize = self.execute_command(
|
659
873
|
["pdm", "self", "add", "keyring"], capture_output=True, text=True
|
660
874
|
)
|
661
875
|
if authorize.returncode > 0:
|
662
|
-
|
663
|
-
"
|
876
|
+
error = PublishError(
|
877
|
+
message="Authentication setup failed",
|
878
|
+
error_code=ErrorCode.AUTHENTICATION_ERROR,
|
879
|
+
details=f"Failed to add keyring support to PDM.\nCommand output:\n{authorize.stderr}",
|
880
|
+
recovery="Please manually add your keyring credentials to PDM. Run `pdm self add keyring` and try again.",
|
881
|
+
exit_code=1,
|
882
|
+
)
|
883
|
+
handle_error(
|
884
|
+
error=error,
|
885
|
+
console=self.console,
|
886
|
+
verbose=options.verbose,
|
887
|
+
ai_agent=options.ai_agent,
|
664
888
|
)
|
665
|
-
|
889
|
+
|
666
890
|
build = self.execute_command(
|
667
891
|
["pdm", "build"], capture_output=True, text=True
|
668
892
|
)
|
669
893
|
self.console.print(build.stdout)
|
894
|
+
|
670
895
|
if build.returncode > 0:
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
896
|
+
error = PublishError(
|
897
|
+
message="Package build failed",
|
898
|
+
error_code=ErrorCode.BUILD_ERROR,
|
899
|
+
details=f"Command output:\n{build.stderr}",
|
900
|
+
recovery="Review the error message above for details. Common issues include missing dependencies, invalid project structure, or incorrect metadata in pyproject.toml.",
|
901
|
+
exit_code=1,
|
902
|
+
)
|
903
|
+
handle_error(
|
904
|
+
error=error,
|
905
|
+
console=self.console,
|
906
|
+
verbose=options.verbose,
|
907
|
+
ai_agent=options.ai_agent,
|
908
|
+
)
|
909
|
+
|
910
|
+
publish_result = self.execute_command(
|
911
|
+
["pdm", "publish", "--no-build"], capture_output=True, text=True
|
912
|
+
)
|
913
|
+
|
914
|
+
if publish_result.returncode > 0:
|
915
|
+
error = PublishError(
|
916
|
+
message="Package publication failed",
|
917
|
+
error_code=ErrorCode.PUBLISH_ERROR,
|
918
|
+
details=f"Command output:\n{publish_result.stderr}",
|
919
|
+
recovery="Ensure you have the correct PyPI credentials configured. Check your internet connection and that the package name is available on PyPI.",
|
920
|
+
exit_code=1,
|
921
|
+
)
|
922
|
+
handle_error(
|
923
|
+
error=error,
|
924
|
+
console=self.console,
|
925
|
+
verbose=options.verbose,
|
926
|
+
ai_agent=options.ai_agent,
|
927
|
+
)
|
928
|
+
else:
|
929
|
+
self.console.print("[green]✅ Package published successfully![/green]")
|
675
930
|
|
676
931
|
def _commit_and_push(self, options: OptionsProtocol) -> None:
|
677
932
|
if options.commit:
|