crackerjack 0.15.7__py3-none-any.whl → 0.15.9__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 CHANGED
@@ -1,4 +1,3 @@
1
- # Created by .ignore support plugin (hsz.mobi)
2
1
  /.idea
3
2
  /.pdm-python
4
3
  /.pdm.toml
@@ -11,6 +10,5 @@
11
10
  /tmp/
12
11
  /__pycache__/
13
12
  /*.pyc
14
- /.crackerjack.yaml
15
13
  /scratch/
16
14
  /.zencoder/
@@ -1,6 +1,6 @@
1
1
  repos:
2
2
  - repo: https://github.com/pdm-project/pdm
3
- rev: 2.24.0 # a PDM release exposing the hook
3
+ rev: 2.24.1 # a PDM release exposing the hook
4
4
  hooks:
5
5
  - id: pdm-lock-check
6
6
  # - id: pdm-export
@@ -23,7 +23,7 @@ repos:
23
23
  - id: check-added-large-files
24
24
  name: check-added-large-files
25
25
  - repo: https://github.com/astral-sh/ruff-pre-commit
26
- rev: v0.11.6
26
+ rev: v0.11.7
27
27
  hooks:
28
28
  - id: ruff-format
29
29
  - id: ruff
@@ -71,11 +71,11 @@ repos:
71
71
  - id: bandit
72
72
  args: ["-c", "pyproject.toml"]
73
73
  - repo: https://github.com/RobertCraigie/pyright-python
74
- rev: v1.1.399
74
+ rev: v1.1.400
75
75
  hooks:
76
76
  - id: pyright
77
77
  - repo: https://github.com/astral-sh/ruff-pre-commit
78
- rev: v0.11.6
78
+ rev: v0.11.7
79
79
  hooks:
80
80
  - id: ruff
81
81
  - id: ruff-format
crackerjack/__main__.py CHANGED
@@ -32,6 +32,7 @@ class Options(BaseModel):
32
32
  clean: bool = False
33
33
  test: bool = False
34
34
  all: BumpOption | None = None
35
+ ai_agent: bool = False
35
36
 
36
37
  @classmethod
37
38
  @field_validator("publish", "bump", mode="before")
@@ -88,6 +89,12 @@ cli_options = {
88
89
  help="Run with `-x -t -p <micro|minor|major> -c` development options).",
89
90
  case_sensitive=False,
90
91
  ),
92
+ "ai_agent": typer.Option(
93
+ False,
94
+ "--ai-agent",
95
+ help="Enable AI agent mode with structured output.",
96
+ hidden=True,
97
+ ),
91
98
  }
92
99
 
93
100
 
@@ -104,6 +111,7 @@ def main(
104
111
  bump: BumpOption | None = cli_options["bump"],
105
112
  clean: bool = cli_options["clean"],
106
113
  test: bool = cli_options["test"],
114
+ ai_agent: bool = cli_options["ai_agent"],
107
115
  ) -> None:
108
116
  options = Options(
109
117
  commit=commit,
@@ -117,8 +125,14 @@ def main(
117
125
  clean=clean,
118
126
  test=test,
119
127
  all=all,
128
+ ai_agent=ai_agent,
120
129
  )
121
130
 
131
+ if ai_agent:
132
+ import os
133
+
134
+ os.environ["AI_AGENT"] = "1"
135
+
122
136
  runner = create_crackerjack_runner(console=console)
123
137
  runner.process(options)
124
138
 
@@ -1,7 +1,9 @@
1
1
  import io
2
+ import os
2
3
  import platform
3
4
  import re
4
5
  import subprocess
6
+ import time
5
7
  import tokenize
6
8
  import typing as t
7
9
  from contextlib import suppress
@@ -40,6 +42,7 @@ class OptionsProtocol(t.Protocol):
40
42
  publish: t.Any | None
41
43
  bump: t.Any | None
42
44
  all: t.Any | None
45
+ ai_agent: bool = False
43
46
 
44
47
 
45
48
  @dataclass
@@ -53,7 +56,7 @@ class CodeCleaner:
53
56
  if not str(file_path.parent).startswith("__"):
54
57
  self.clean_file(file_path)
55
58
  if pkg_dir.parent.joinpath("__pycache__").exists():
56
- pkg_dir.parent.joinpath("__pycache__").rmdir()
59
+ pkg_dir.parent.joinpath("__pycache__").unlink()
57
60
 
58
61
  def clean_file(self, file_path: Path) -> None:
59
62
  try:
@@ -486,22 +489,161 @@ class Crackerjack:
486
489
  self.console.print("\nCleaning tests directory...\n")
487
490
  self.code_cleaner.clean_files(tests_dir)
488
491
 
489
- def _run_tests(self, options: OptionsProtocol) -> None:
490
- if options.test:
491
- self.console.print("\n\nRunning tests...\n")
492
- test = ["pytest"]
493
- if options.verbose:
494
- test.append("-v")
495
- result = self.execute_command(test, capture_output=True, text=True)
496
- if result.stdout:
497
- self.console.print(result.stdout)
498
- if result.returncode > 0:
499
- if result.stderr:
500
- self.console.print(result.stderr)
492
+ def _prepare_pytest_command(self, options: OptionsProtocol) -> list[str]:
493
+ """Prepare the pytest command with appropriate options."""
494
+ test = ["pytest"]
495
+ if options.verbose:
496
+ test.append("-v")
497
+
498
+ # Add optimized options for all packages to prevent hanging
499
+ test.extend(
500
+ [
501
+ "--no-cov", # Disable coverage which can cause hanging
502
+ "--capture=fd", # Capture stdout/stderr at file descriptor level
503
+ "--tb=short", # Shorter traceback format
504
+ "--no-header", # Reduce output noise
505
+ "--disable-warnings", # Disable warning capture
506
+ "--durations=0", # Show slowest tests to identify potential hanging tests
507
+ "--timeout=300", # 5-minute timeout for tests
508
+ ]
509
+ )
510
+ return test
511
+
512
+ def _setup_test_environment(self) -> None:
513
+ """Set environment variables for test execution."""
514
+ # Set environment variables to improve asyncio behavior
515
+ os.environ["PYTHONASYNCIO_DEBUG"] = "0" # Disable asyncio debug mode
516
+ os.environ["RUNNING_UNDER_CRACKERJACK"] = "1" # Signal to conftest.py
517
+
518
+ # Set asyncio mode to strict to help prevent hanging
519
+ if "PYTEST_ASYNCIO_MODE" not in os.environ:
520
+ os.environ["PYTEST_ASYNCIO_MODE"] = "strict"
521
+
522
+ def _run_pytest_process(
523
+ self, test_command: list[str]
524
+ ) -> subprocess.CompletedProcess[str]:
525
+ """Run pytest as a subprocess with timeout handling and output capture."""
526
+ try:
527
+ # Use a timeout to ensure the process doesn't hang indefinitely
528
+ process = subprocess.Popen(
529
+ test_command,
530
+ stdout=subprocess.PIPE,
531
+ stderr=subprocess.PIPE,
532
+ text=True,
533
+ bufsize=1,
534
+ universal_newlines=True,
535
+ )
536
+
537
+ # Set a timeout (5 minutes)
538
+ timeout = 300
539
+ start_time = time.time()
540
+
541
+ stdout_data = []
542
+ stderr_data = []
543
+
544
+ # Read output while process is running, with timeout
545
+ while process.poll() is None:
546
+ if time.time() - start_time > timeout:
547
+ self.console.print(
548
+ "[red]Test execution timed out after 5 minutes. Terminating...[/red]"
549
+ )
550
+ process.terminate()
551
+ try:
552
+ process.wait(timeout=5)
553
+ except subprocess.TimeoutExpired:
554
+ process.kill()
555
+ break
556
+
557
+ # Read output without blocking
558
+ if process.stdout:
559
+ line = process.stdout.readline()
560
+ if line:
561
+ stdout_data.append(line)
562
+ self.console.print(line, end="")
563
+
564
+ if process.stderr:
565
+ line = process.stderr.readline()
566
+ if line:
567
+ stderr_data.append(line)
568
+ self.console.print(f"[red]{line}[/red]", end="")
569
+
570
+ time.sleep(0.1)
571
+
572
+ # Get any remaining output
573
+ if process.stdout:
574
+ for line in process.stdout:
575
+ stdout_data.append(line)
576
+ self.console.print(line, end="")
577
+
578
+ if process.stderr:
579
+ for line in process.stderr:
580
+ stderr_data.append(line)
581
+ self.console.print(f"[red]{line}[/red]", end="")
582
+
583
+ returncode = process.returncode or 0
584
+ stdout = "".join(stdout_data)
585
+ stderr = "".join(stderr_data)
586
+
587
+ # Create a CompletedProcess object to match the expected interface
588
+ return subprocess.CompletedProcess(
589
+ args=test_command, returncode=returncode, stdout=stdout, stderr=stderr
590
+ )
591
+
592
+ except Exception as e:
593
+ self.console.print(f"[red]Error running tests: {e}[/red]")
594
+ return subprocess.CompletedProcess(test_command, 1, "", str(e))
595
+
596
+ def _report_test_results(
597
+ self, result: subprocess.CompletedProcess[str], ai_agent: str
598
+ ) -> None:
599
+ """Report test results and handle AI agent output if needed."""
600
+ if result.returncode > 0:
601
+ if result.stderr:
602
+ self.console.print(result.stderr)
603
+
604
+ if ai_agent:
605
+ # Use structured output for AI agents
606
+ self.console.print(
607
+ '[json]{"status": "failed", "action": "tests", "returncode": '
608
+ + str(result.returncode)
609
+ + "}[/json]"
610
+ )
611
+ else:
501
612
  self.console.print("\n\n❌ Tests failed. Please fix errors.\n")
502
- raise SystemExit(1)
613
+ raise SystemExit(1)
614
+
615
+ if ai_agent:
616
+ # Use structured output for AI agents
617
+ self.console.print('[json]{"status": "success", "action": "tests"}[/json]')
618
+ else:
503
619
  self.console.print("\n\n✅ Tests passed successfully!\n")
504
620
 
621
+ def _run_tests(self, options: OptionsProtocol) -> None:
622
+ """Run tests if the test option is enabled."""
623
+ if options.test:
624
+ # Check if we're being called by an AI agent
625
+ ai_agent = os.environ.get("AI_AGENT", "")
626
+
627
+ if ai_agent:
628
+ # Use structured output for AI agents
629
+ self.console.print(
630
+ '[json]{"status": "running", "action": "tests"}[/json]'
631
+ )
632
+ else:
633
+ self.console.print("\n\nRunning tests...\n")
634
+
635
+ # Prepare the test command
636
+ test_command = self._prepare_pytest_command(options)
637
+
638
+ # Set up the test environment
639
+ self._setup_test_environment()
640
+
641
+ # Run the tests
642
+ result = self._run_pytest_process(test_command)
643
+
644
+ # Report the results
645
+ self._report_test_results(result, ai_agent)
646
+
505
647
  def _bump_version(self, options: OptionsProtocol) -> None:
506
648
  for option in (options.publish, options.bump):
507
649
  if option:
@@ -546,22 +688,65 @@ class Crackerjack:
546
688
  return execute(cmd, **kwargs)
547
689
 
548
690
  def process(self, options: OptionsProtocol) -> None:
691
+ # Track actions performed for AI agent output
692
+ actions_performed = []
693
+
549
694
  if options.all:
550
695
  options.clean = True
551
696
  options.test = True
552
697
  options.publish = options.all
553
698
  options.commit = True
699
+
554
700
  self._setup_package()
701
+ actions_performed.append("setup_package")
702
+
555
703
  self._update_project(options)
704
+ actions_performed.append("update_project")
705
+
556
706
  self._update_precommit(options)
707
+ if options.update_precommit:
708
+ actions_performed.append("update_precommit")
709
+
557
710
  self._run_interactive_hooks(options)
711
+ if options.interactive:
712
+ actions_performed.append("run_interactive_hooks")
713
+
558
714
  self._clean_project(options)
715
+ if options.clean:
716
+ actions_performed.append("clean_project")
717
+
559
718
  self.project_manager.run_pre_commit()
719
+ actions_performed.append("run_pre_commit")
720
+
560
721
  self._run_tests(options)
722
+ if options.test:
723
+ actions_performed.append("run_tests")
724
+
561
725
  self._bump_version(options)
726
+ if options.bump or options.publish:
727
+ actions_performed.append("bump_version")
728
+
562
729
  self._publish_project(options)
730
+ if options.publish:
731
+ actions_performed.append("publish_project")
732
+
563
733
  self._commit_and_push(options)
564
- self.console.print("\n🍺 Crackerjack complete!\n")
734
+ if options.commit:
735
+ actions_performed.append("commit_and_push")
736
+
737
+ # Check if we're being called by an AI agent
738
+ if getattr(options, "ai_agent", False):
739
+ # Use structured output for AI agents
740
+ import json
741
+
742
+ result = {
743
+ "status": "complete",
744
+ "package": self.pkg_name,
745
+ "actions": actions_performed,
746
+ }
747
+ self.console.print(f"[json]{json.dumps(result)}[/json]")
748
+ else:
749
+ self.console.print("\n🍺 Crackerjack complete!\n")
565
750
 
566
751
 
567
752
  def create_crackerjack_runner(
@@ -72,7 +72,7 @@ no-lines-before = [
72
72
  ]
73
73
 
74
74
  [tool.ruff.lint.mccabe]
75
- max-complexity = 12
75
+ max-complexity = 13
76
76
 
77
77
  [tool.ruff.lint.pydocstyle]
78
78
  convention = "google"
@@ -101,6 +101,7 @@ exclude-deps = [
101
101
  "uv",
102
102
  "tomli-w",
103
103
  "google-crc32c",
104
+ "pytest-timeout",
104
105
  ]
105
106
 
106
107
  [tool.refurb]
@@ -149,7 +150,7 @@ pythonPlatform = "Darwin"
149
150
 
150
151
  [project]
151
152
  name = "crackerjack"
152
- version = "0.15.6"
153
+ version = "0.15.8"
153
154
  description = "Default template for PDM package"
154
155
  requires-python = ">=3.13"
155
156
  readme = "README.md"
@@ -180,14 +181,15 @@ dependencies = [
180
181
  "pytest>=8.3.5",
181
182
  "pydantic>=2.11.3",
182
183
  "pdm-bump>=0.9.12",
183
- "pdm>=2.24.0",
184
- "uv>=0.6.14",
184
+ "pdm>=2.24.1",
185
+ "uv>=0.6.17",
185
186
  "pytest-cov>=6.1.1",
186
187
  "pytest-mock>=3.14.0",
187
188
  "tomli-w>=1.2.0",
188
189
  "pytest-asyncio>=0.26.0",
189
190
  "rich>=14.0.0",
190
191
  "typer>=0.15.2",
192
+ "pytest-timeout>=2.3.1",
191
193
  ]
192
194
  authors = [
193
195
  { name = "lesleslie", email = "les@wedgwoodwebworks.com" },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crackerjack
3
- Version: 0.15.7
3
+ Version: 0.15.9
4
4
  Summary: Default template for PDM package
5
5
  Keywords: black,ruff,mypy,creosote,refurb
6
6
  Author-Email: lesleslie <les@wedgwoodwebworks.com>
@@ -27,20 +27,21 @@ Requires-Dist: pre-commit>=4.2.0
27
27
  Requires-Dist: pytest>=8.3.5
28
28
  Requires-Dist: pydantic>=2.11.3
29
29
  Requires-Dist: pdm-bump>=0.9.12
30
- Requires-Dist: pdm>=2.24.0
31
- Requires-Dist: uv>=0.6.14
30
+ Requires-Dist: pdm>=2.24.1
31
+ Requires-Dist: uv>=0.6.17
32
32
  Requires-Dist: pytest-cov>=6.1.1
33
33
  Requires-Dist: pytest-mock>=3.14.0
34
34
  Requires-Dist: tomli-w>=1.2.0
35
35
  Requires-Dist: pytest-asyncio>=0.26.0
36
36
  Requires-Dist: rich>=14.0.0
37
37
  Requires-Dist: typer>=0.15.2
38
+ Requires-Dist: pytest-timeout>=2.3.1
38
39
  Description-Content-Type: text/markdown
39
40
 
40
41
  # Crackerjack: Elevate Your Python Development
41
42
 
42
43
  [![Code style: crackerjack](https://img.shields.io/badge/code%20style-crackerjack-000042)](https://github.com/lesleslie/crackerjack)
43
- [![Python Version](https://img.shields.io/badge/python-3.13-blue.svg)](https://www.python.org/downloads/)
44
+ [![Python: 3.13+](https://img.shields.io/badge/python-3.13%2B-green)](https://www.python.org/downloads/)
44
45
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
45
46
  [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
46
47
  [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
@@ -1,12 +1,12 @@
1
- crackerjack-0.15.7.dist-info/METADATA,sha256=1bdpFF2yF_9rktzsL5TnNXyAVreZ938C_UuqAA8IRSo,12863
2
- crackerjack-0.15.7.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- crackerjack-0.15.7.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- crackerjack-0.15.7.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
1
+ crackerjack-0.15.9.dist-info/METADATA,sha256=1I_OWQRrzahxxT8f5F8EieXlGDXSuAD0F9JGpMfhbco,12899
2
+ crackerjack-0.15.9.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ crackerjack-0.15.9.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ crackerjack-0.15.9.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
5
5
  crackerjack/.coverage,sha256=dLzPzp72qZEXohNfxnOAlRwvM9dqF06-HoFqfvXZd1U,53248
6
- crackerjack/.gitignore,sha256=ts3GBu94chiwTyOCNCMHau9M8XGrd1yajyHPAMo_Z84,219
6
+ crackerjack/.gitignore,sha256=oho3dNx7a7y36_y9AsalCkssU4in0MMsNAANWdc-h1c,153
7
7
  crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
8
8
  crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
9
- crackerjack/.pre-commit-config.yaml,sha256=mpTonkoUwZAvITz6yzpsm-PTsKub8SUELz35V6leUlo,2456
9
+ crackerjack/.pre-commit-config.yaml,sha256=69iH07bdH1eF45gNrJAp3LKQkmxTg38lniLg9U9oVq4,2456
10
10
  crackerjack/.pytest_cache/.gitignore,sha256=Ptcxtl0GFQwTji2tsL4Gl1UIiKa0frjEXsya26i46b0,37
11
11
  crackerjack/.pytest_cache/CACHEDIR.TAG,sha256=N9yI75oKvt2-gQU6bdj9-xOvthMEXqHrSlyBWnSjveQ,191
12
12
  crackerjack/.pytest_cache/README.md,sha256=c_1vzN2ALEGaay2YPWwxc7fal1WKxLWJ7ewt_kQ9ua0,302
@@ -23,6 +23,8 @@ crackerjack/.ruff_cache/0.11.2/4070660268492669020,sha256=FTRTUmvj6nZw_QQBp_WHI-
23
23
  crackerjack/.ruff_cache/0.11.3/9818742842212983150,sha256=U-4mT__a-OljovvAJvv5M6X7TCMa3dReLXx3kTNGgwU,224
24
24
  crackerjack/.ruff_cache/0.11.4/9818742842212983150,sha256=QF9j6-3MH_d0pDNotdbF2hlqCL66SxN8OLVKR3PZyZU,224
25
25
  crackerjack/.ruff_cache/0.11.6/3557596832929915217,sha256=yR2iXWDkSHVRw2eTiaCE8Eh34JPRUGc8vE3HYEEBk9k,224
26
+ crackerjack/.ruff_cache/0.11.7/10386934055395314831,sha256=lBNwN5zAgM4OzbkXIOzCczUtfooATrD10htj9ASlFkc,224
27
+ crackerjack/.ruff_cache/0.11.7/3557596832929915217,sha256=fKlwUbsvT3YIKV6UR-aA_i64lLignWeVfVu-MMmVbU0,207
26
28
  crackerjack/.ruff_cache/0.2.0/10047773857155985907,sha256=j9LNa_RQ4Plor7go1uTYgz17cEENKvZQ-dP6b9MX0ik,248
27
29
  crackerjack/.ruff_cache/0.2.1/8522267973936635051,sha256=u_aPBMibtAp_iYvLwR88GMAECMcIgHezxMyuapmU2P4,248
28
30
  crackerjack/.ruff_cache/0.2.2/18053836298936336950,sha256=Xb_ebP0pVuUfSqPEZKlhQ70so_vqkEfMYpuHQ06iR5U,248
@@ -51,7 +53,7 @@ crackerjack/.ruff_cache/0.9.9/12813592349865671909,sha256=tmr8_vhRD2OxsVuMfbJPdT
51
53
  crackerjack/.ruff_cache/0.9.9/8843823720003377982,sha256=e4ymkXfQsUg5e_mtO34xTsaTvs1uA3_fI216Qq9qCAM,136
52
54
  crackerjack/.ruff_cache/CACHEDIR.TAG,sha256=WVMVbX4MVkpCclExbq8m-IcOZIOuIZf5FrYw5Pk-Ma4,43
53
55
  crackerjack/__init__.py,sha256=r9SuEjHUrW99hFWifRk4ofmYPSgf9rblcnzqhdV5bP0,157
54
- crackerjack/__main__.py,sha256=iwoUyUB9j3e5lC5ViNAqW00eeoIx0xljNmWC04AiPaw,3707
55
- crackerjack/crackerjack.py,sha256=vyE3Ja42FmIlQMO10F1qBgl2KbSqMtTO4qs-HnW7kAk,22503
56
- crackerjack/pyproject.toml,sha256=cdAZIpMICUD8wQV1g6wYBZD-9OUl_3bq856BuU4tO10,4089
57
- crackerjack-0.15.7.dist-info/RECORD,,
56
+ crackerjack/__main__.py,sha256=vgylaqzoqYUjYstAOQLOU-odCtYarb2oAFBQdzYdar0,4037
57
+ crackerjack/crackerjack.py,sha256=IHe1JDIS3Ng_0VW4SjS0HNaKQrRLEu6KAr0bMOFZrro,29114
58
+ crackerjack/pyproject.toml,sha256=uUQOPlQd4ULCUAMcurTger5BHxiZZJg3d6Z764HVXTQ,4140
59
+ crackerjack-0.15.9.dist-info/RECORD,,