crackerjack 0.27.0__tar.gz → 0.27.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (45) hide show
  1. {crackerjack-0.27.0/crackerjack → crackerjack-0.27.1}/.pre-commit-config.yaml +0 -12
  2. {crackerjack-0.27.0 → crackerjack-0.27.1}/CLAUDE.md +8 -0
  3. {crackerjack-0.27.0 → crackerjack-0.27.1}/PKG-INFO +1 -1
  4. {crackerjack-0.27.0 → crackerjack-0.27.1}/RULES.md +3 -2
  5. {crackerjack-0.27.0 → crackerjack-0.27.1/crackerjack}/.pre-commit-config.yaml +0 -12
  6. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/__main__.py +3 -2
  7. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/crackerjack.py +241 -85
  8. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/interactive.py +1 -1
  9. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/py313.py +2 -2
  10. {crackerjack-0.27.0 → crackerjack-0.27.1/crackerjack}/pyproject.toml +0 -18
  11. {crackerjack-0.27.0/crackerjack → crackerjack-0.27.1}/pyproject.toml +1 -19
  12. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_crackerjack.py +1 -0
  13. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_crackerjack_runner.py +1 -0
  14. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_structured_errors.py +1 -0
  15. {crackerjack-0.27.0 → crackerjack-0.27.1}/uv.lock +1 -1
  16. {crackerjack-0.27.0 → crackerjack-0.27.1}/.envrc +0 -0
  17. {crackerjack-0.27.0 → crackerjack-0.27.1}/.github/FUNDING.yml +0 -0
  18. {crackerjack-0.27.0 → crackerjack-0.27.1}/.gitignore +0 -0
  19. {crackerjack-0.27.0 → crackerjack-0.27.1}/.libcst.codemod.yaml +0 -0
  20. {crackerjack-0.27.0 → crackerjack-0.27.1}/.pre-commit-config-ai.yaml +0 -0
  21. {crackerjack-0.27.0 → crackerjack-0.27.1}/.pre-commit-config-fast.yaml +0 -0
  22. {crackerjack-0.27.0 → crackerjack-0.27.1}/LICENSE +0 -0
  23. {crackerjack-0.27.0 → crackerjack-0.27.1}/README-AI-AGENT.md +0 -0
  24. {crackerjack-0.27.0 → crackerjack-0.27.1}/README.md +0 -0
  25. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/.gitignore +0 -0
  26. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/.libcst.codemod.yaml +0 -0
  27. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/.pdm.toml +0 -0
  28. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/.pre-commit-config-ai.yaml +0 -0
  29. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/__init__.py +0 -0
  30. {crackerjack-0.27.0 → crackerjack-0.27.1}/crackerjack/errors.py +0 -0
  31. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/TESTING.md +0 -0
  32. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/__init__.py +0 -0
  33. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/conftest.py +0 -0
  34. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/data/comments_sample.txt +0 -0
  35. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/data/docstrings_sample.txt +0 -0
  36. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/data/expected_comments_sample.txt +0 -0
  37. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/data/init.py +0 -0
  38. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_errors.py +0 -0
  39. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_interactive.py +0 -0
  40. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_interactive_run.py +0 -0
  41. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_main.py +0 -0
  42. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_multiline_functions.py +0 -0
  43. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_py313_advanced.py +0 -0
  44. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_py313_features.py +0 -0
  45. {crackerjack-0.27.0 → crackerjack-0.27.1}/tests/test_pytest_features.py +0 -0
@@ -91,10 +91,6 @@ repos:
91
91
  - --aggressive
92
92
  - --only-without-imports
93
93
  - --guess-common-names
94
- - --cache-dir=.autotyping-cache
95
- - --workers=4
96
- - --max-line-length=88
97
- - --exclude-name=test_*,conftest
98
94
  - crackerjack
99
95
  types_or: [ python, pyi ]
100
96
  language: python
@@ -116,11 +112,3 @@ repos:
116
112
  hooks:
117
113
  - id: pyright
118
114
  stages: [pre-push, manual]
119
-
120
- # Additional quality and security checks
121
- - repo: https://github.com/PyCQA/pydocstyle
122
- rev: 6.3.0
123
- hooks:
124
- - id: pydocstyle
125
- args: ["--config=pyproject.toml"]
126
- stages: [pre-push, manual]
@@ -333,6 +333,7 @@ Crackerjack follows a single-file architecture for simplicity and maintainabilit
333
333
  1. **Code Style**: Follow the Crackerjack style guide (see RULES.md):
334
334
 
335
335
  - **Target Python 3.13+** - Use latest Python features
336
+ - **NO DOCSTRINGS** - The codebase standard is to have no docstrings (they are removed by the `-x` flag)
336
337
  - Use static typing throughout with modern syntax (import typing as `t`)
337
338
  - Use pathlib for file operations
338
339
  - Prefer Protocol over ABC
@@ -372,6 +373,13 @@ When generating code, AI assistants MUST follow these standards to ensure compli
372
373
 
373
374
  **IMPORTANT: Target Python 3.13+** - All code must be compatible with Python 3.13 or newer. Use the latest Python features and syntax.
374
375
 
376
+ **CRITICAL: NO DOCSTRINGS** - Crackerjack's standard is to have NO docstrings in the package code. The `-x` (clean) flag removes all docstrings to reduce noise and keep the codebase clean. When generating code:
377
+
378
+ - **DO NOT** add docstrings to functions, methods, classes, or modules
379
+ - **DO NOT** add triple-quoted string documentation anywhere
380
+ - Use inline comments sparingly only when absolutely necessary for complex logic
381
+ - The codebase prioritizes clean, self-documenting code over documentation strings
382
+
375
383
  ### Refurb Standards (Modern Python Patterns up to 3.12)
376
384
 
377
385
  **Use modern syntax and built-ins:**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crackerjack
3
- Version: 0.27.0
3
+ Version: 0.27.1
4
4
  Summary: Crackerjack: code quality toolkit
5
5
  Project-URL: documentation, https://github.com/lesleslie/crackerjack
6
6
  Project-URL: homepage, https://github.com/lesleslie/crackerjack
@@ -23,9 +23,10 @@
23
23
  - **Clean Code Architecture**
24
24
 
25
25
  - Write modular functions that do one thing well
26
- - Avoid unnecessary docstrings and line comments
26
+ - **NO DOCSTRINGS**: Never add docstrings to any code - the codebase standard is to have no docstrings (they are automatically removed by the `-x` flag)
27
+ - Avoid unnecessary line comments - use them sparingly only for complex logic
27
28
  - Use protocols (`t.Protocol`) instead of abstract base classes
28
- - Choose clear, descriptive variable and function names
29
+ - Choose clear, descriptive variable and function names that make the code self-documenting
29
30
 
30
31
  - **Code Organization**
31
32
 
@@ -91,10 +91,6 @@ repos:
91
91
  - --aggressive
92
92
  - --only-without-imports
93
93
  - --guess-common-names
94
- - --cache-dir=.autotyping-cache
95
- - --workers=4
96
- - --max-line-length=88
97
- - --exclude-name=test_*,conftest
98
94
  - crackerjack
99
95
  types_or: [ python, pyi ]
100
96
  language: python
@@ -116,11 +112,3 @@ repos:
116
112
  hooks:
117
113
  - id: pyright
118
114
  stages: [pre-push, manual]
119
-
120
- # Additional quality and security checks
121
- - repo: https://github.com/PyCQA/pydocstyle
122
- rev: 6.3.0
123
- hooks:
124
- - id: pydocstyle
125
- args: ["--config=pyproject.toml"]
126
- stages: [pre-push, manual]
@@ -4,7 +4,8 @@ from enum import Enum
4
4
  import typer
5
5
  from pydantic import BaseModel, field_validator
6
6
  from rich.console import Console
7
- from crackerjack import create_crackerjack_runner
7
+
8
+ from .crackerjack import create_crackerjack_runner
8
9
 
9
10
  console = Console(force_terminal=True)
10
11
  app = typer.Typer(
@@ -206,7 +207,7 @@ def main(
206
207
 
207
208
  os.environ["AI_AGENT"] = "1"
208
209
  if interactive:
209
- from crackerjack.interactive import launch_interactive_cli
210
+ from .interactive import launch_interactive_cli
210
211
 
211
212
  try:
212
213
  from importlib.metadata import version
@@ -18,7 +18,8 @@ import aiofiles
18
18
  from pydantic import BaseModel
19
19
  from rich.console import Console
20
20
  from tomli_w import dumps
21
- from crackerjack.errors import ErrorCode, ExecutionError
21
+
22
+ from .errors import ErrorCode, ExecutionError
22
23
 
23
24
 
24
25
  @dataclass
@@ -361,7 +362,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
361
362
 
362
363
  def remove_docstrings(self, code: str) -> str:
363
364
  lines = code.split("\n")
364
- cleaned_lines = []
365
+ cleaned_lines: list[str] = []
365
366
  docstring_state = self._initialize_docstring_state()
366
367
  for i, line in enumerate(lines):
367
368
  handled, result_line = self._process_line(lines, i, line, docstring_state)
@@ -418,7 +419,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
418
419
 
419
420
  def remove_line_comments(self, code: str) -> str:
420
421
  lines = code.split("\n")
421
- cleaned_lines = []
422
+ cleaned_lines: list[str] = []
422
423
  for line in lines:
423
424
  if not line.strip():
424
425
  cleaned_lines.append(line)
@@ -429,7 +430,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
429
430
  return "\n".join(cleaned_lines)
430
431
 
431
432
  def _process_line_for_comments(self, line: str) -> str:
432
- result = []
433
+ result: list[str] = []
433
434
  string_state = {"in_string": None}
434
435
  for i, char in enumerate(line):
435
436
  if self._handle_string_character(char, i, line, string_state, result):
@@ -483,7 +484,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
483
484
 
484
485
  def remove_extra_whitespace(self, code: str) -> str:
485
486
  lines = code.split("\n")
486
- cleaned_lines = []
487
+ cleaned_lines: list[str] = []
487
488
  function_tracker = {"in_function": False, "function_indent": 0}
488
489
  import_tracker = {"in_imports": False, "last_import_type": None}
489
490
  for i, line in enumerate(lines):
@@ -854,8 +855,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
854
855
  from crackerjack.errors import ExecutionError, handle_error
855
856
 
856
857
  try:
857
- async with aiofiles.open(file_path, encoding="utf-8") as f:
858
- code = await f.read()
858
+ async with aiofiles.open(file_path, encoding="utf-8") as f: # type: ignore[misc]
859
+ code = await f.read() # type: ignore[misc]
859
860
  original_code = code
860
861
  cleaning_failed = False
861
862
  try:
@@ -890,8 +891,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
890
891
  )
891
892
  code = original_code
892
893
  cleaning_failed = True
893
- async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
894
- await f.write(code)
894
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f: # type: ignore[misc]
895
+ await f.write(code) # type: ignore[misc]
895
896
  if cleaning_failed:
896
897
  self.console.print(
897
898
  f"[bold yellow]⚡ Partially cleaned:[/bold yellow] [dim bright_white]{file_path}[/dim bright_white]"
@@ -965,8 +966,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
965
966
  suffix=".py", mode="w+", delete=False
966
967
  ) as temp:
967
968
  temp_path = Path(temp.name)
968
- async with aiofiles.open(temp_path, "w", encoding="utf-8") as f:
969
- await f.write(code)
969
+ async with aiofiles.open(temp_path, "w", encoding="utf-8") as f: # type: ignore[misc]
970
+ await f.write(code) # type: ignore[misc]
970
971
  try:
971
972
  proc = await asyncio.create_subprocess_exec(
972
973
  "uv",
@@ -977,10 +978,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
977
978
  stdout=asyncio.subprocess.PIPE,
978
979
  stderr=asyncio.subprocess.PIPE,
979
980
  )
980
- stdout, stderr = await proc.communicate()
981
+ _, stderr = await proc.communicate()
981
982
  if proc.returncode == 0:
982
- async with aiofiles.open(temp_path, encoding="utf-8") as f:
983
- formatted_code = await f.read()
983
+ async with aiofiles.open(temp_path, encoding="utf-8") as f: # type: ignore[misc]
984
+ formatted_code = await f.read() # type: ignore[misc]
984
985
  else:
985
986
  self.console.print(
986
987
  f"[bold bright_yellow]⚠️ Warning: Ruff format failed with return code {proc.returncode}[/bold bright_yellow]"
@@ -1025,7 +1026,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
1025
1026
  return code
1026
1027
 
1027
1028
  async def _cleanup_cache_directories_async(self, pkg_dir: Path) -> None:
1028
- def cleanup_sync():
1029
+ def cleanup_sync() -> None:
1029
1030
  with suppress(PermissionError, OSError):
1030
1031
  pycache_dir = pkg_dir / "__pycache__"
1031
1032
  if pycache_dir.exists():
@@ -1171,7 +1172,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1171
1172
  ) -> None:
1172
1173
  python_version_pattern = "\\s*W*(\\d\\.\\d*)"
1173
1174
  requires_python = our_toml_config.get("project", {}).get("requires-python", "")
1174
- classifiers = []
1175
+ classifiers: list[str] = []
1175
1176
  for classifier in pkg_toml_config.get("project", {}).get("classifiers", []):
1176
1177
  classifier = re.sub(
1177
1178
  python_version_pattern, f" {self.python_version}", classifier
@@ -1186,7 +1187,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1186
1187
  self.pkg_toml_path.write_text(dumps(pkg_toml_config))
1187
1188
 
1188
1189
  def copy_configs(self) -> None:
1189
- configs_to_add = []
1190
+ configs_to_add: list[str] = []
1190
1191
  for config in config_files:
1191
1192
  config_path = self.our_path / config
1192
1193
  pkg_config_path = self.pkg_path / config
@@ -1238,7 +1239,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1238
1239
  len(py_files) + len(js_files) + len(yaml_files) + len(md_files)
1239
1240
  )
1240
1241
  total_size = 0
1241
- for files in [py_files, js_files, yaml_files, md_files]:
1242
+ for files in (py_files, js_files, yaml_files, md_files):
1242
1243
  for file_path in files:
1243
1244
  try:
1244
1245
  total_size += file_path.stat().st_size
@@ -1395,7 +1396,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1395
1396
  return hook_results
1396
1397
 
1397
1398
  def _parse_hook_output(self, stdout: str, stderr: str) -> list[HookResult]:
1398
- hook_results = []
1399
+ hook_results: list[HookResult] = []
1399
1400
  lines = stdout.split("\n")
1400
1401
  for line in lines:
1401
1402
  if "..." in line and (
@@ -1482,7 +1483,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1482
1483
  def _generate_optimization_suggestions(
1483
1484
  self, hook_results: list[HookResult]
1484
1485
  ) -> list[str]:
1485
- suggestions = []
1486
+ suggestions: list[str] = []
1486
1487
 
1487
1488
  for hook in hook_results:
1488
1489
  if hook.duration > 5.0:
@@ -1578,35 +1579,40 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1578
1579
  def _parse_refurb_results(self) -> dict[str, t.Any]:
1579
1580
  return {"suggestions": 0, "patterns_modernized": []}
1580
1581
 
1582
+ def _parse_complexity_list(
1583
+ self, complexity_data: list[dict[str, t.Any]]
1584
+ ) -> dict[str, t.Any]:
1585
+ if not complexity_data:
1586
+ return {
1587
+ "average_complexity": 0,
1588
+ "max_complexity": 0,
1589
+ "total_functions": 0,
1590
+ }
1591
+ complexities = [item.get("complexity", 0) for item in complexity_data]
1592
+ return {
1593
+ "average_complexity": sum(complexities) / len(complexities)
1594
+ if complexities
1595
+ else 0,
1596
+ "max_complexity": max(complexities) if complexities else 0,
1597
+ "total_functions": len(complexities),
1598
+ }
1599
+
1600
+ def _parse_complexity_dict(
1601
+ self, complexity_data: dict[str, t.Any]
1602
+ ) -> dict[str, t.Any]:
1603
+ return {
1604
+ "average_complexity": complexity_data.get("average", 0),
1605
+ "max_complexity": complexity_data.get("max", 0),
1606
+ "total_functions": complexity_data.get("total", 0),
1607
+ }
1608
+
1581
1609
  def _parse_complexity_results(self) -> dict[str, t.Any]:
1582
1610
  try:
1583
1611
  with open("complexipy.json", encoding="utf-8") as f:
1584
1612
  complexity_data = json.load(f)
1585
1613
  if isinstance(complexity_data, list):
1586
- if not complexity_data:
1587
- return {
1588
- "average_complexity": 0,
1589
- "max_complexity": 0,
1590
- "total_functions": 0,
1591
- }
1592
- complexities = [
1593
- item.get("complexity", 0)
1594
- for item in complexity_data
1595
- if isinstance(item, dict)
1596
- ]
1597
- return {
1598
- "average_complexity": sum(complexities) / len(complexities)
1599
- if complexities
1600
- else 0,
1601
- "max_complexity": max(complexities) if complexities else 0,
1602
- "total_functions": len(complexities),
1603
- }
1604
- else:
1605
- return {
1606
- "average_complexity": complexity_data.get("average", 0),
1607
- "max_complexity": complexity_data.get("max", 0),
1608
- "total_functions": complexity_data.get("total", 0),
1609
- }
1614
+ return self._parse_complexity_list(complexity_data)
1615
+ return self._parse_complexity_dict(complexity_data)
1610
1616
  except (FileNotFoundError, json.JSONDecodeError):
1611
1617
  return {"status": "complexity_analysis_not_available"}
1612
1618
 
@@ -1653,7 +1659,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1653
1659
  return 95.0
1654
1660
 
1655
1661
  def _generate_quality_recommendations(self) -> list[str]:
1656
- recommendations = []
1662
+ recommendations: list[str] = []
1657
1663
  recommendations.extend(
1658
1664
  [
1659
1665
  "Consider adding more integration tests",
@@ -1749,18 +1755,16 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1749
1755
  return "unknown"
1750
1756
 
1751
1757
  def _analyze_directory_structure(self) -> dict[str, t.Any]:
1752
- directories = []
1753
- for item in self.pkg_path.iterdir():
1754
- if item.is_dir() and not item.name.startswith(
1755
- (".git", "__pycache__", ".venv")
1756
- ):
1757
- directories.append(
1758
- {
1759
- "name": item.name,
1760
- "type": self._classify_directory(item),
1761
- "file_count": len(list(item.rglob("*"))),
1762
- }
1763
- )
1758
+ directories = [
1759
+ {
1760
+ "name": item.name,
1761
+ "type": self._classify_directory(item),
1762
+ "file_count": len(list(item.rglob("*"))),
1763
+ }
1764
+ for item in self.pkg_path.iterdir()
1765
+ if item.is_dir()
1766
+ and not item.name.startswith((".git", "__pycache__", ".venv"))
1767
+ ]
1764
1768
  return {"directories": directories, "total_directories": len(directories)}
1765
1769
 
1766
1770
  def _analyze_file_distribution(self) -> dict[str, t.Any]:
@@ -1778,17 +1782,15 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1778
1782
 
1779
1783
  def _analyze_dependencies(self) -> dict[str, t.Any]:
1780
1784
  deps = {"status": "analysis_not_implemented"}
1781
- try:
1785
+ with suppress(Exception):
1782
1786
  pyproject_path = self.pkg_path / "pyproject.toml"
1783
1787
  if pyproject_path.exists():
1784
1788
  pyproject_path.read_text(encoding="utf-8")
1785
1789
  deps = {"source": "pyproject.toml", "status": "detected"}
1786
- except Exception:
1787
- pass
1788
1790
  return deps
1789
1791
 
1790
1792
  def _analyze_configuration_files(self) -> list[str]:
1791
- config_files = []
1793
+ config_files: list[str] = []
1792
1794
  config_patterns = ["*.toml", "*.yaml", "*.yml", "*.ini", "*.cfg", ".env*"]
1793
1795
  for pattern in config_patterns:
1794
1796
  config_files.extend([f.name for f in self.pkg_path.glob(pattern)])
@@ -1859,8 +1861,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1859
1861
  return "hidden"
1860
1862
  elif (directory / "__init__.py").exists():
1861
1863
  return "python_package"
1862
- else:
1863
- return "general"
1864
+ return "general"
1864
1865
 
1865
1866
  def _collect_environment_info(self) -> dict[str, t.Any]:
1866
1867
  return {
@@ -1871,7 +1872,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1871
1872
  }
1872
1873
 
1873
1874
  def _identify_common_issues(self) -> list[str]:
1874
- issues = []
1875
+ issues: list[str] = []
1875
1876
  if not (self.pkg_path / "pyproject.toml").exists():
1876
1877
  issues.append("Missing pyproject.toml configuration")
1877
1878
  if not (self.pkg_path / ".gitignore").exists():
@@ -2048,6 +2049,58 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
2048
2049
 
2049
2050
  return hook_results
2050
2051
 
2052
+ def _get_analysis_files(self) -> list[str]:
2053
+ analysis_files: list[str] = []
2054
+ if (
2055
+ hasattr(self, "options")
2056
+ and self.options
2057
+ and getattr(self.options, "ai_agent", False)
2058
+ ):
2059
+ analysis_files.extend(
2060
+ [
2061
+ "test-results.xml",
2062
+ "coverage.json",
2063
+ "benchmark.json",
2064
+ "ai-agent-summary.json",
2065
+ ]
2066
+ )
2067
+ return analysis_files
2068
+
2069
+ def _generate_analysis_files(self, hook_results: list[HookResult]) -> None:
2070
+ if not (
2071
+ hasattr(self, "options")
2072
+ and self.options
2073
+ and getattr(self.options, "ai_agent", False)
2074
+ ):
2075
+ return
2076
+ try:
2077
+ import json
2078
+
2079
+ summary = {
2080
+ "status": "success"
2081
+ if all(hr.status == "Passed" for hr in hook_results)
2082
+ else "failed",
2083
+ "hook_results": [
2084
+ {
2085
+ "name": hr.name,
2086
+ "status": hr.status,
2087
+ "duration": hr.duration,
2088
+ "issues": hr.issues_found
2089
+ if hasattr(hr, "issues_found")
2090
+ else [],
2091
+ }
2092
+ for hr in hook_results
2093
+ ],
2094
+ "total_duration": sum(hr.duration for hr in hook_results),
2095
+ "files_analyzed": len(hook_results),
2096
+ }
2097
+ with open("ai-agent-summary.json", "w") as f:
2098
+ json.dump(summary, f, indent=2)
2099
+ except Exception as e:
2100
+ self.console.print(
2101
+ f"[yellow]Warning: Failed to generate AI summary: {e}[/yellow]"
2102
+ )
2103
+
2051
2104
 
2052
2105
  class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2053
2106
  our_path: Path = Path(__file__).parent
@@ -2096,6 +2149,8 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2096
2149
  "[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]"
2097
2150
  )
2098
2151
  self.console.print("-" * 80 + "\n")
2152
+ assert self.config_manager is not None
2153
+ assert self.project_manager is not None
2099
2154
  self.config_manager.pkg_name = self.pkg_name
2100
2155
  self.project_manager.pkg_name = self.pkg_name
2101
2156
  self.project_manager.pkg_dir = self.pkg_dir
@@ -2309,31 +2364,38 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2309
2364
  except (OSError, PermissionError):
2310
2365
  return "medium"
2311
2366
 
2367
+ def _calculate_test_metrics(self, test_files: list[Path]) -> tuple[int, int]:
2368
+ total_test_size = 0
2369
+ slow_tests = 0
2370
+ for test_file in test_files:
2371
+ try:
2372
+ size = test_file.stat().st_size
2373
+ total_test_size += size
2374
+ if size > 30_000 or "integration" in test_file.name.lower():
2375
+ slow_tests += 1
2376
+ except (OSError, PermissionError):
2377
+ continue
2378
+ return total_test_size, slow_tests
2379
+
2380
+ def _determine_test_complexity(
2381
+ self, test_count: int, avg_size: float, slow_ratio: float
2382
+ ) -> str:
2383
+ if test_count > 100 or avg_size > 25_000 or slow_ratio > 0.4:
2384
+ return "high"
2385
+ elif test_count > 50 or avg_size > 15_000 or slow_ratio > 0.2:
2386
+ return "medium"
2387
+ return "low"
2388
+
2312
2389
  def _analyze_test_workload(self) -> dict[str, t.Any]:
2313
2390
  try:
2314
2391
  test_files = self._get_cached_files_with_mtime("test_*.py")
2315
2392
  py_files = self._get_cached_files_with_mtime("*.py")
2316
- total_test_size = 0
2317
- slow_tests = 0
2318
- for test_file in test_files:
2319
- try:
2320
- size = test_file.stat().st_size
2321
- total_test_size += size
2322
- if size > 30_000 or "integration" in test_file.name.lower():
2323
- slow_tests += 1
2324
- except (OSError, PermissionError):
2325
- continue
2393
+ total_test_size, slow_tests = self._calculate_test_metrics(test_files)
2326
2394
  avg_test_size = total_test_size / len(test_files) if test_files else 0
2327
2395
  slow_test_ratio = slow_tests / len(test_files) if test_files else 0
2328
- if len(test_files) > 100 or avg_test_size > 25_000 or slow_test_ratio > 0.4:
2329
- complexity = "high"
2330
- elif (
2331
- len(test_files) > 50 or avg_test_size > 15_000 or slow_test_ratio > 0.2
2332
- ):
2333
- complexity = "medium"
2334
- else:
2335
- complexity = "low"
2336
-
2396
+ complexity = self._determine_test_complexity(
2397
+ len(test_files), avg_test_size, slow_test_ratio
2398
+ )
2337
2399
  return {
2338
2400
  "total_files": len(py_files),
2339
2401
  "test_files": len(test_files),
@@ -2354,8 +2416,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2354
2416
  return min(cpu_count // 3, 2)
2355
2417
  elif workload["complexity"] == "medium":
2356
2418
  return min(cpu_count // 2, 4)
2357
- else:
2358
- return min(cpu_count, 8)
2419
+ return min(cpu_count, 8)
2359
2420
 
2360
2421
  def _print_ai_agent_files(self, options: t.Any) -> None:
2361
2422
  if getattr(options, "ai_agent", False):
@@ -2507,6 +2568,99 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2507
2568
  stderr.decode() if stderr else "",
2508
2569
  )
2509
2570
 
2571
+ def _run_comprehensive_quality_checks(self, options: OptionsProtocol) -> None:
2572
+ if options.skip_hooks or (
2573
+ options.test
2574
+ and not any([options.publish, options.bump, options.commit, options.all])
2575
+ ):
2576
+ return
2577
+ needs_comprehensive = any(
2578
+ [options.publish, options.bump, options.commit, options.all]
2579
+ )
2580
+ if not needs_comprehensive:
2581
+ return
2582
+ self.console.print("\n" + "-" * 80)
2583
+ self.console.print(
2584
+ "[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
2585
+ )
2586
+ self.console.print("-" * 80 + "\n")
2587
+ cmd = [
2588
+ "uv",
2589
+ "run",
2590
+ "pre-commit",
2591
+ "run",
2592
+ "--all-files",
2593
+ "--hook-stage=manual",
2594
+ "-c",
2595
+ ".pre-commit-config.yaml",
2596
+ ]
2597
+ result = self.execute_command(cmd, capture_output=True, text=True)
2598
+ if result.returncode > 0:
2599
+ self.console.print(
2600
+ "\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
2601
+ )
2602
+ self.console.print(f"[dim]STDOUT:[/dim]\n{result.stdout}")
2603
+ if result.stderr:
2604
+ self.console.print(f"[dim]STDERR:[/dim]\n{result.stderr}")
2605
+ self.console.print(
2606
+ "\n[bold red]Cannot proceed with publishing/committing until all quality checks pass. [/bold red]"
2607
+ )
2608
+ raise SystemExit(1)
2609
+ else:
2610
+ self.console.print(
2611
+ "[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
2612
+ )
2613
+
2614
+ async def _run_comprehensive_quality_checks_async(
2615
+ self, options: OptionsProtocol
2616
+ ) -> None:
2617
+ if options.skip_hooks or (
2618
+ options.test
2619
+ and not any([options.publish, options.bump, options.commit, options.all])
2620
+ ):
2621
+ return
2622
+
2623
+ needs_comprehensive = any(
2624
+ [options.publish, options.bump, options.commit, options.all]
2625
+ )
2626
+
2627
+ if not needs_comprehensive:
2628
+ return
2629
+
2630
+ self.console.print("\n" + "-" * 80)
2631
+ self.console.print(
2632
+ "[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
2633
+ )
2634
+ self.console.print("-" * 80 + "\n")
2635
+
2636
+ cmd = [
2637
+ "uv",
2638
+ "run",
2639
+ "pre-commit",
2640
+ "run",
2641
+ "--all-files",
2642
+ "--hook-stage=manual",
2643
+ "-c",
2644
+ ".pre-commit-config.yaml",
2645
+ ]
2646
+
2647
+ result = await self.execute_command_async(cmd)
2648
+
2649
+ if result.returncode > 0:
2650
+ self.console.print(
2651
+ "\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
2652
+ )
2653
+ if result.stderr:
2654
+ self.console.print(f"[dim]Error details: {result.stderr}[/dim]")
2655
+ self.console.print(
2656
+ "\n[bold red]Cannot proceed with publishing/committing until all quality checks pass. [/bold red]"
2657
+ )
2658
+ raise SystemExit(1)
2659
+ else:
2660
+ self.console.print(
2661
+ "[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
2662
+ )
2663
+
2510
2664
  def process(self, options: OptionsProtocol) -> None:
2511
2665
  self.console.print("\n" + "-" * 80)
2512
2666
  self.console.print(
@@ -2533,6 +2687,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2533
2687
  "\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
2534
2688
  )
2535
2689
  self._run_tests(options)
2690
+ self._run_comprehensive_quality_checks(options)
2536
2691
  self._bump_version(options)
2537
2692
  self._publish_project(options)
2538
2693
  self._commit_and_push(options)
@@ -2568,6 +2723,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2568
2723
  "\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
2569
2724
  )
2570
2725
  await self._run_tests_async(options)
2726
+ await self._run_comprehensive_quality_checks_async(options)
2571
2727
  self._bump_version(options)
2572
2728
  self._publish_project(options)
2573
2729
  self._commit_and_push(options)
@@ -85,7 +85,7 @@ class WorkflowManager:
85
85
  def add_task(
86
86
  self, name: str, description: str, dependencies: list[str] | None = None
87
87
  ) -> Task:
88
- dep_tasks = []
88
+ dep_tasks: list[Task] = []
89
89
  if dependencies:
90
90
  for dep_name in dependencies:
91
91
  if dep_name not in self.tasks:
@@ -109,7 +109,7 @@ def process_hook_results[T, R](
109
109
  success_handler: typing.Callable[[T], R],
110
110
  failure_handler: typing.Callable[[T], R],
111
111
  ) -> list[R]:
112
- processed_results = []
112
+ processed_results: list[R] = []
113
113
  for result in results:
114
114
  if isinstance(result, dict) and result.get("status") == HookStatus.SUCCESS:
115
115
  processed_results.append(success_handler(result))
@@ -156,7 +156,7 @@ class EnhancedCommandRunner:
156
156
 
157
157
  def clean_python_code(code: str) -> str:
158
158
  lines = code.splitlines()
159
- cleaned_lines = []
159
+ cleaned_lines: list[str] = []
160
160
  for line in lines:
161
161
  match line.strip():
162
162
  case "":
@@ -81,19 +81,11 @@ output-format = "full"
81
81
  format.docstring-code-format = true
82
82
  lint.extend-select = [
83
83
  "C901",
84
- "D",
85
84
  "F", # pyflakes
86
85
  "I",
87
86
  "UP", # pyupgrade (includes F-string conversion)
88
87
  ]
89
88
  lint.ignore = [
90
- "D100", # Missing docstring in public module - can be safely enabled
91
- "D101", # Missing docstring in public class - conflicts with code cleaning
92
- "D102", # Missing docstring in public method - conflicts with code cleaning
93
- "D103", # Missing docstring in public function - conflicts with code cleaning
94
- "D104", # Missing docstring in public package - package level docs
95
- "D105", # Missing docstring in magic method - conflicts with code cleaning
96
- "D107", # Missing docstring in __init__ - conflicts with code cleaning
97
89
  "E402", # Module level import not at top - sometimes necessary
98
90
  "F821", # Undefined name - can be resolved with proper imports
99
91
  ]
@@ -103,7 +95,6 @@ lint.isort.no-lines-before = [
103
95
  "first-party",
104
96
  ]
105
97
  lint.mccabe.max-complexity = 13
106
- lint.pydocstyle.convention = "google"
107
98
 
108
99
  [tool.codespell]
109
100
  skip = "*/data/*"
@@ -271,15 +262,6 @@ exclude_dirs = [
271
262
  "tests/data", # Test data might contain examples
272
263
  ]
273
264
 
274
- # Enhanced documentation quality
275
-
276
- [tool.pydocstyle]
277
- convention = "google"
278
- add-ignore = [
279
- "D100", # Missing docstring in public module
280
- "D104", # Missing docstring in public package
281
- ]
282
-
283
265
  # Autotyping optimization
284
266
 
285
267
  [tool.autotyping]
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.26.0"
7
+ version = "0.27.1"
8
8
  description = "Crackerjack: code quality toolkit"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -81,19 +81,11 @@ output-format = "full"
81
81
  format.docstring-code-format = true
82
82
  lint.extend-select = [
83
83
  "C901",
84
- "D",
85
84
  "F", # pyflakes
86
85
  "I",
87
86
  "UP", # pyupgrade (includes F-string conversion)
88
87
  ]
89
88
  lint.ignore = [
90
- "D100", # Missing docstring in public module - can be safely enabled
91
- "D101", # Missing docstring in public class - conflicts with code cleaning
92
- "D102", # Missing docstring in public method - conflicts with code cleaning
93
- "D103", # Missing docstring in public function - conflicts with code cleaning
94
- "D104", # Missing docstring in public package - package level docs
95
- "D105", # Missing docstring in magic method - conflicts with code cleaning
96
- "D107", # Missing docstring in __init__ - conflicts with code cleaning
97
89
  "E402", # Module level import not at top - sometimes necessary
98
90
  "F821", # Undefined name - can be resolved with proper imports
99
91
  ]
@@ -103,7 +95,6 @@ lint.isort.no-lines-before = [
103
95
  "first-party",
104
96
  ]
105
97
  lint.mccabe.max-complexity = 13
106
- lint.pydocstyle.convention = "google"
107
98
 
108
99
  [tool.codespell]
109
100
  skip = "*/data/*"
@@ -271,15 +262,6 @@ exclude_dirs = [
271
262
  "tests/data", # Test data might contain examples
272
263
  ]
273
264
 
274
- # Enhanced documentation quality
275
-
276
- [tool.pydocstyle]
277
- convention = "google"
278
- add-ignore = [
279
- "D100", # Missing docstring in public module
280
- "D104", # Missing docstring in public package
281
- ]
282
-
283
265
  # Autotyping optimization
284
266
 
285
267
  [tool.autotyping]
@@ -49,6 +49,7 @@ class OptionsForTesting:
49
49
  create_pr: bool = False
50
50
  skip_hooks: bool = False
51
51
  comprehensive: bool = False
52
+ async_mode: bool = False
52
53
 
53
54
 
54
55
  @pytest.fixture
@@ -32,6 +32,7 @@ class MockOptions:
32
32
  self.test_workers = kwargs.get("test_workers", 0)
33
33
  self.test_timeout = kwargs.get("test_timeout", 0)
34
34
  self.comprehensive = kwargs.get("comprehensive", False)
35
+ self.async_mode = kwargs.get("async_mode", False)
35
36
 
36
37
 
37
38
  def test_create_crackerjack_runner() -> None:
@@ -136,6 +136,7 @@ class TestErrorHandlingIntegration:
136
136
  create_pr = False
137
137
  skip_hooks = False
138
138
  comprehensive = False
139
+ async_mode = False
139
140
 
140
141
  options = Options()
141
142
  with patch("platform.system", return_value="Linux"):
@@ -114,7 +114,7 @@ wheels = [
114
114
 
115
115
  [[package]]
116
116
  name = "crackerjack"
117
- version = "0.27.0"
117
+ version = "0.27.1"
118
118
  source = { editable = "." }
119
119
  dependencies = [
120
120
  { name = "aiofiles" },
File without changes
File without changes
File without changes
File without changes