crackerjack 0.27.0__tar.gz → 0.27.3__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.3}/.gitignore +3 -0
  2. {crackerjack-0.27.0/crackerjack → crackerjack-0.27.3}/.pre-commit-config.yaml +0 -12
  3. {crackerjack-0.27.0 → crackerjack-0.27.3}/CLAUDE.md +8 -0
  4. {crackerjack-0.27.0 → crackerjack-0.27.3}/PKG-INFO +1 -1
  5. {crackerjack-0.27.0 → crackerjack-0.27.3}/RULES.md +3 -2
  6. {crackerjack-0.27.0 → crackerjack-0.27.3/crackerjack}/.gitignore +3 -0
  7. {crackerjack-0.27.0 → crackerjack-0.27.3/crackerjack}/.pre-commit-config.yaml +0 -12
  8. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/__main__.py +3 -2
  9. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/crackerjack.py +360 -99
  10. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/interactive.py +1 -1
  11. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/py313.py +7 -3
  12. {crackerjack-0.27.0 → crackerjack-0.27.3/crackerjack}/pyproject.toml +1 -19
  13. {crackerjack-0.27.0/crackerjack → crackerjack-0.27.3}/pyproject.toml +1 -19
  14. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_crackerjack.py +1 -0
  15. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_crackerjack_runner.py +1 -0
  16. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_structured_errors.py +1 -0
  17. {crackerjack-0.27.0 → crackerjack-0.27.3}/uv.lock +1 -1
  18. {crackerjack-0.27.0 → crackerjack-0.27.3}/.envrc +0 -0
  19. {crackerjack-0.27.0 → crackerjack-0.27.3}/.github/FUNDING.yml +0 -0
  20. {crackerjack-0.27.0 → crackerjack-0.27.3}/.libcst.codemod.yaml +0 -0
  21. {crackerjack-0.27.0 → crackerjack-0.27.3}/.pre-commit-config-ai.yaml +0 -0
  22. {crackerjack-0.27.0 → crackerjack-0.27.3}/.pre-commit-config-fast.yaml +0 -0
  23. {crackerjack-0.27.0 → crackerjack-0.27.3}/LICENSE +0 -0
  24. {crackerjack-0.27.0 → crackerjack-0.27.3}/README-AI-AGENT.md +0 -0
  25. {crackerjack-0.27.0 → crackerjack-0.27.3}/README.md +0 -0
  26. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/.libcst.codemod.yaml +0 -0
  27. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/.pdm.toml +0 -0
  28. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/.pre-commit-config-ai.yaml +0 -0
  29. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/__init__.py +0 -0
  30. {crackerjack-0.27.0 → crackerjack-0.27.3}/crackerjack/errors.py +0 -0
  31. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/TESTING.md +0 -0
  32. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/__init__.py +0 -0
  33. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/conftest.py +0 -0
  34. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/data/comments_sample.txt +0 -0
  35. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/data/docstrings_sample.txt +0 -0
  36. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/data/expected_comments_sample.txt +0 -0
  37. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/data/init.py +0 -0
  38. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_errors.py +0 -0
  39. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_interactive.py +0 -0
  40. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_interactive_run.py +0 -0
  41. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_main.py +0 -0
  42. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_multiline_functions.py +0 -0
  43. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_py313_advanced.py +0 -0
  44. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_py313_features.py +0 -0
  45. {crackerjack-0.27.0 → crackerjack-0.27.3}/tests/test_pytest_features.py +0 -0
@@ -29,3 +29,6 @@
29
29
 
30
30
  # Autotyping cache
31
31
  .autotyping-cache/
32
+
33
+ # Crackerjack state file
34
+ .crackerjack-state
@@ -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.3
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
 
@@ -29,3 +29,6 @@
29
29
 
30
30
  # Autotyping cache
31
31
  .autotyping-cache/
32
+
33
+ # Crackerjack state file
34
+ .crackerjack-state
@@ -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):
@@ -539,9 +540,15 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
539
540
 
540
541
  def process_lines():
541
542
  lines = code.split("\n")
542
- function_tracker = {"in_function": False, "function_indent": 0}
543
- import_tracker = {"in_imports": False, "last_import_type": None}
544
- previous_lines = []
543
+ function_tracker: dict[str, t.Any] = {
544
+ "in_function": False,
545
+ "function_indent": 0,
546
+ }
547
+ import_tracker: dict[str, t.Any] = {
548
+ "in_imports": False,
549
+ "last_import_type": None,
550
+ }
551
+ previous_lines: list[str] = []
545
552
  for i, line in enumerate(lines):
546
553
  line = line.rstrip()
547
554
  stripped_line = line.lstrip()
@@ -854,9 +861,9 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
854
861
  from crackerjack.errors import ExecutionError, handle_error
855
862
 
856
863
  try:
857
- async with aiofiles.open(file_path, encoding="utf-8") as f:
858
- code = await f.read()
859
- original_code = code
864
+ async with aiofiles.open(file_path, encoding="utf-8") as f: # type: ignore[misc]
865
+ code: str = await f.read() # type: ignore[misc]
866
+ original_code: str = code
860
867
  cleaning_failed = False
861
868
  try:
862
869
  code = self.remove_line_comments_streaming(code)
@@ -890,8 +897,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
890
897
  )
891
898
  code = original_code
892
899
  cleaning_failed = True
893
- async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
894
- await f.write(code)
900
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as f: # type: ignore[misc]
901
+ await f.write(code) # type: ignore[misc]
895
902
  if cleaning_failed:
896
903
  self.console.print(
897
904
  f"[bold yellow]⚡ Partially cleaned:[/bold yellow] [dim bright_white]{file_path}[/dim bright_white]"
@@ -965,8 +972,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
965
972
  suffix=".py", mode="w+", delete=False
966
973
  ) as temp:
967
974
  temp_path = Path(temp.name)
968
- async with aiofiles.open(temp_path, "w", encoding="utf-8") as f:
969
- await f.write(code)
975
+ async with aiofiles.open(temp_path, "w", encoding="utf-8") as f: # type: ignore[misc]
976
+ await f.write(code) # type: ignore[misc]
970
977
  try:
971
978
  proc = await asyncio.create_subprocess_exec(
972
979
  "uv",
@@ -977,10 +984,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
977
984
  stdout=asyncio.subprocess.PIPE,
978
985
  stderr=asyncio.subprocess.PIPE,
979
986
  )
980
- stdout, stderr = await proc.communicate()
987
+ _, stderr = await proc.communicate()
981
988
  if proc.returncode == 0:
982
- async with aiofiles.open(temp_path, encoding="utf-8") as f:
983
- formatted_code = await f.read()
989
+ async with aiofiles.open(temp_path, encoding="utf-8") as f: # type: ignore[misc]
990
+ formatted_code = await f.read() # type: ignore[misc]
984
991
  else:
985
992
  self.console.print(
986
993
  f"[bold bright_yellow]⚠️ Warning: Ruff format failed with return code {proc.returncode}[/bold bright_yellow]"
@@ -1025,7 +1032,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
1025
1032
  return code
1026
1033
 
1027
1034
  async def _cleanup_cache_directories_async(self, pkg_dir: Path) -> None:
1028
- def cleanup_sync():
1035
+ def cleanup_sync() -> None:
1029
1036
  with suppress(PermissionError, OSError):
1030
1037
  pycache_dir = pkg_dir / "__pycache__"
1031
1038
  if pycache_dir.exists():
@@ -1114,7 +1121,9 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1114
1121
  ) -> None:
1115
1122
  for setting, value in our_config.items():
1116
1123
  if isinstance(value, dict):
1117
- self._merge_nested_config(setting, value, pkg_config)
1124
+ self._merge_nested_config(
1125
+ setting, t.cast(dict[str, t.Any], value), pkg_config
1126
+ )
1118
1127
  else:
1119
1128
  self._merge_direct_config(setting, value, pkg_config)
1120
1129
 
@@ -1133,11 +1142,13 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1133
1142
  self, key: str, value: t.Any, nested_config: dict[str, t.Any]
1134
1143
  ) -> None:
1135
1144
  if isinstance(value, str | list) and "crackerjack" in str(value):
1136
- nested_config[key] = self.swap_package_name(value)
1145
+ nested_config[key] = self.swap_package_name(t.cast(str | list[str], value))
1137
1146
  elif self._is_mergeable_list(key, value):
1138
1147
  existing = nested_config.get(key, [])
1139
1148
  if isinstance(existing, list) and isinstance(value, list):
1140
- nested_config[key] = list(set(existing + value))
1149
+ nested_config[key] = list(
1150
+ set(t.cast(list[str], existing) + t.cast(list[str], value))
1151
+ )
1141
1152
  else:
1142
1153
  nested_config[key] = value
1143
1154
  elif key not in nested_config:
@@ -1147,11 +1158,13 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1147
1158
  self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
1148
1159
  ) -> None:
1149
1160
  if isinstance(value, str | list) and "crackerjack" in str(value):
1150
- pkg_config[setting] = self.swap_package_name(value)
1161
+ pkg_config[setting] = self.swap_package_name(t.cast(str | list[str], value))
1151
1162
  elif self._is_mergeable_list(setting, value):
1152
1163
  existing = pkg_config.get(setting, [])
1153
1164
  if isinstance(existing, list) and isinstance(value, list):
1154
- pkg_config[setting] = list(set(existing + value))
1165
+ pkg_config[setting] = list(
1166
+ set(t.cast(list[str], existing) + t.cast(list[str], value))
1167
+ )
1155
1168
  else:
1156
1169
  pkg_config[setting] = value
1157
1170
  elif setting not in pkg_config:
@@ -1171,7 +1184,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1171
1184
  ) -> None:
1172
1185
  python_version_pattern = "\\s*W*(\\d\\.\\d*)"
1173
1186
  requires_python = our_toml_config.get("project", {}).get("requires-python", "")
1174
- classifiers = []
1187
+ classifiers: list[str] = []
1175
1188
  for classifier in pkg_toml_config.get("project", {}).get("classifiers", []):
1176
1189
  classifier = re.sub(
1177
1190
  python_version_pattern, f" {self.python_version}", classifier
@@ -1186,7 +1199,7 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
1186
1199
  self.pkg_toml_path.write_text(dumps(pkg_toml_config))
1187
1200
 
1188
1201
  def copy_configs(self) -> None:
1189
- configs_to_add = []
1202
+ configs_to_add: list[str] = []
1190
1203
  for config in config_files:
1191
1204
  config_path = self.our_path / config
1192
1205
  pkg_config_path = self.pkg_path / config
@@ -1238,7 +1251,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1238
1251
  len(py_files) + len(js_files) + len(yaml_files) + len(md_files)
1239
1252
  )
1240
1253
  total_size = 0
1241
- for files in [py_files, js_files, yaml_files, md_files]:
1254
+ for files in (py_files, js_files, yaml_files, md_files):
1242
1255
  for file_path in files:
1243
1256
  try:
1244
1257
  total_size += file_path.stat().st_size
@@ -1395,7 +1408,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1395
1408
  return hook_results
1396
1409
 
1397
1410
  def _parse_hook_output(self, stdout: str, stderr: str) -> list[HookResult]:
1398
- hook_results = []
1411
+ hook_results: list[HookResult] = []
1399
1412
  lines = stdout.split("\n")
1400
1413
  for line in lines:
1401
1414
  if "..." in line and (
@@ -1482,7 +1495,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1482
1495
  def _generate_optimization_suggestions(
1483
1496
  self, hook_results: list[HookResult]
1484
1497
  ) -> list[str]:
1485
- suggestions = []
1498
+ suggestions: list[str] = []
1486
1499
 
1487
1500
  for hook in hook_results:
1488
1501
  if hook.duration > 5.0:
@@ -1578,35 +1591,42 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1578
1591
  def _parse_refurb_results(self) -> dict[str, t.Any]:
1579
1592
  return {"suggestions": 0, "patterns_modernized": []}
1580
1593
 
1594
+ def _parse_complexity_list(
1595
+ self, complexity_data: list[dict[str, t.Any]]
1596
+ ) -> dict[str, t.Any]:
1597
+ if not complexity_data:
1598
+ return {
1599
+ "average_complexity": 0,
1600
+ "max_complexity": 0,
1601
+ "total_functions": 0,
1602
+ }
1603
+ complexities = [item.get("complexity", 0) for item in complexity_data]
1604
+ return {
1605
+ "average_complexity": sum(complexities) / len(complexities)
1606
+ if complexities
1607
+ else 0,
1608
+ "max_complexity": max(complexities) if complexities else 0,
1609
+ "total_functions": len(complexities),
1610
+ }
1611
+
1612
+ def _parse_complexity_dict(
1613
+ self, complexity_data: dict[str, t.Any]
1614
+ ) -> dict[str, t.Any]:
1615
+ return {
1616
+ "average_complexity": complexity_data.get("average", 0),
1617
+ "max_complexity": complexity_data.get("max", 0),
1618
+ "total_functions": complexity_data.get("total", 0),
1619
+ }
1620
+
1581
1621
  def _parse_complexity_results(self) -> dict[str, t.Any]:
1582
1622
  try:
1583
1623
  with open("complexipy.json", encoding="utf-8") as f:
1584
1624
  complexity_data = json.load(f)
1585
1625
  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
- }
1626
+ return self._parse_complexity_list(
1627
+ t.cast(list[dict[str, t.Any]], complexity_data)
1628
+ )
1629
+ return self._parse_complexity_dict(complexity_data)
1610
1630
  except (FileNotFoundError, json.JSONDecodeError):
1611
1631
  return {"status": "complexity_analysis_not_available"}
1612
1632
 
@@ -1653,7 +1673,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1653
1673
  return 95.0
1654
1674
 
1655
1675
  def _generate_quality_recommendations(self) -> list[str]:
1656
- recommendations = []
1676
+ recommendations: list[str] = []
1657
1677
  recommendations.extend(
1658
1678
  [
1659
1679
  "Consider adding more integration tests",
@@ -1749,22 +1769,20 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1749
1769
  return "unknown"
1750
1770
 
1751
1771
  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
- )
1772
+ directories = [
1773
+ {
1774
+ "name": item.name,
1775
+ "type": self._classify_directory(item),
1776
+ "file_count": len(list(item.rglob("*"))),
1777
+ }
1778
+ for item in self.pkg_path.iterdir()
1779
+ if item.is_dir()
1780
+ and not item.name.startswith((".git", "__pycache__", ".venv"))
1781
+ ]
1764
1782
  return {"directories": directories, "total_directories": len(directories)}
1765
1783
 
1766
1784
  def _analyze_file_distribution(self) -> dict[str, t.Any]:
1767
- file_types = {}
1785
+ file_types: dict[str, int] = {}
1768
1786
  total_files = 0
1769
1787
  for file_path in self.pkg_path.rglob("*"):
1770
1788
  if file_path.is_file() and not str(file_path).startswith(
@@ -1778,17 +1796,15 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1778
1796
 
1779
1797
  def _analyze_dependencies(self) -> dict[str, t.Any]:
1780
1798
  deps = {"status": "analysis_not_implemented"}
1781
- try:
1799
+ with suppress(Exception):
1782
1800
  pyproject_path = self.pkg_path / "pyproject.toml"
1783
1801
  if pyproject_path.exists():
1784
1802
  pyproject_path.read_text(encoding="utf-8")
1785
1803
  deps = {"source": "pyproject.toml", "status": "detected"}
1786
- except Exception:
1787
- pass
1788
1804
  return deps
1789
1805
 
1790
1806
  def _analyze_configuration_files(self) -> list[str]:
1791
- config_files = []
1807
+ config_files: list[str] = []
1792
1808
  config_patterns = ["*.toml", "*.yaml", "*.yml", "*.ini", "*.cfg", ".env*"]
1793
1809
  for pattern in config_patterns:
1794
1810
  config_files.extend([f.name for f in self.pkg_path.glob(pattern)])
@@ -1859,8 +1875,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1859
1875
  return "hidden"
1860
1876
  elif (directory / "__init__.py").exists():
1861
1877
  return "python_package"
1862
- else:
1863
- return "general"
1878
+ return "general"
1864
1879
 
1865
1880
  def _collect_environment_info(self) -> dict[str, t.Any]:
1866
1881
  return {
@@ -1871,7 +1886,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
1871
1886
  }
1872
1887
 
1873
1888
  def _identify_common_issues(self) -> list[str]:
1874
- issues = []
1889
+ issues: list[str] = []
1875
1890
  if not (self.pkg_path / "pyproject.toml").exists():
1876
1891
  issues.append("Missing pyproject.toml configuration")
1877
1892
  if not (self.pkg_path / ".gitignore").exists():
@@ -2048,6 +2063,58 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
2048
2063
 
2049
2064
  return hook_results
2050
2065
 
2066
+ def _get_analysis_files(self) -> list[str]:
2067
+ analysis_files: list[str] = []
2068
+ if (
2069
+ hasattr(self, "options")
2070
+ and self.options
2071
+ and getattr(self.options, "ai_agent", False)
2072
+ ):
2073
+ analysis_files.extend(
2074
+ [
2075
+ "test-results.xml",
2076
+ "coverage.json",
2077
+ "benchmark.json",
2078
+ "ai-agent-summary.json",
2079
+ ]
2080
+ )
2081
+ return analysis_files
2082
+
2083
+ def _generate_analysis_files(self, hook_results: list[HookResult]) -> None:
2084
+ if not (
2085
+ hasattr(self, "options")
2086
+ and self.options
2087
+ and getattr(self.options, "ai_agent", False)
2088
+ ):
2089
+ return
2090
+ try:
2091
+ import json
2092
+
2093
+ summary = {
2094
+ "status": "success"
2095
+ if all(hr.status == "Passed" for hr in hook_results)
2096
+ else "failed",
2097
+ "hook_results": [
2098
+ {
2099
+ "name": hr.name,
2100
+ "status": hr.status,
2101
+ "duration": hr.duration,
2102
+ "issues": hr.issues_found
2103
+ if hasattr(hr, "issues_found")
2104
+ else [],
2105
+ }
2106
+ for hr in hook_results
2107
+ ],
2108
+ "total_duration": sum(hr.duration for hr in hook_results),
2109
+ "files_analyzed": len(hook_results),
2110
+ }
2111
+ with open("ai-agent-summary.json", "w") as f:
2112
+ json.dump(summary, f, indent=2)
2113
+ except Exception as e:
2114
+ self.console.print(
2115
+ f"[yellow]Warning: Failed to generate AI summary: {e}[/yellow]"
2116
+ )
2117
+
2051
2118
 
2052
2119
  class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2053
2120
  our_path: Path = Path(__file__).parent
@@ -2062,11 +2129,13 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2062
2129
  project_manager: ProjectManager | None = None
2063
2130
  _file_cache: dict[str, list[Path]] = {}
2064
2131
  _file_cache_with_mtime: dict[str, tuple[float, list[Path]]] = {}
2132
+ _state_file: Path = Path(".crackerjack-state")
2065
2133
 
2066
2134
  def __init__(self, **data: t.Any) -> None:
2067
2135
  super().__init__(**data)
2068
2136
  self._file_cache = {}
2069
2137
  self._file_cache_with_mtime = {}
2138
+ self._state_file = Path(".crackerjack-state")
2070
2139
  self.code_cleaner = CodeCleaner(console=self.console)
2071
2140
  self.config_manager = ConfigManager(
2072
2141
  our_path=self.our_path,
@@ -2087,6 +2156,73 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2087
2156
  dry_run=self.dry_run,
2088
2157
  )
2089
2158
 
2159
+ def _read_state(self) -> dict[str, t.Any]:
2160
+ import json
2161
+
2162
+ if self._state_file.exists():
2163
+ try:
2164
+ return json.loads(self._state_file.read_text(encoding="utf-8"))
2165
+ except (json.JSONDecodeError, OSError):
2166
+ return {}
2167
+ return {}
2168
+
2169
+ def _write_state(self, state: dict[str, t.Any]) -> None:
2170
+ from contextlib import suppress
2171
+
2172
+ with suppress(OSError):
2173
+ import json
2174
+
2175
+ self._state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
2176
+
2177
+ def _clear_state(self) -> None:
2178
+ if self._state_file.exists():
2179
+ from contextlib import suppress
2180
+
2181
+ with suppress(OSError):
2182
+ self._state_file.unlink()
2183
+
2184
+ def _has_version_been_bumped(self, version_type: str) -> bool:
2185
+ state = self._read_state()
2186
+ current_version = self._get_current_version()
2187
+ last_bumped_version = state.get("last_bumped_version")
2188
+ last_bump_type = state.get("last_bump_type")
2189
+
2190
+ return (
2191
+ last_bumped_version == current_version
2192
+ and last_bump_type == version_type
2193
+ and not state.get("publish_completed", False)
2194
+ )
2195
+
2196
+ def _mark_version_bumped(self, version_type: str) -> None:
2197
+ current_version = self._get_current_version()
2198
+ state = self._read_state()
2199
+ state.update(
2200
+ {
2201
+ "last_bumped_version": current_version,
2202
+ "last_bump_type": version_type,
2203
+ "publish_completed": False,
2204
+ }
2205
+ )
2206
+ self._write_state(state)
2207
+
2208
+ def _mark_publish_completed(self) -> None:
2209
+ state = self._read_state()
2210
+ state["publish_completed"] = True
2211
+ self._write_state(state)
2212
+
2213
+ def _get_current_version(self) -> str:
2214
+ from contextlib import suppress
2215
+
2216
+ with suppress(Exception):
2217
+ import tomllib
2218
+
2219
+ pyproject_path = Path("pyproject.toml")
2220
+ if pyproject_path.exists():
2221
+ with pyproject_path.open("rb") as f:
2222
+ data = tomllib.load(f)
2223
+ return data.get("project", {}).get("version", "unknown")
2224
+ return "unknown"
2225
+
2090
2226
  def _setup_package(self) -> None:
2091
2227
  self.pkg_name = self.pkg_path.stem.lower().replace("-", "_")
2092
2228
  self.pkg_dir = self.pkg_path / self.pkg_name
@@ -2096,11 +2232,14 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2096
2232
  "[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]"
2097
2233
  )
2098
2234
  self.console.print("-" * 80 + "\n")
2235
+ assert self.config_manager is not None
2236
+ assert self.project_manager is not None
2099
2237
  self.config_manager.pkg_name = self.pkg_name
2100
2238
  self.project_manager.pkg_name = self.pkg_name
2101
2239
  self.project_manager.pkg_dir = self.pkg_dir
2102
2240
 
2103
2241
  def _update_project(self, options: t.Any) -> None:
2242
+ assert self.project_manager is not None
2104
2243
  if not options.no_config_updates:
2105
2244
  self.project_manager.update_pkg_configs()
2106
2245
  result: CompletedProcess[str] = self.execute_command(
@@ -2123,6 +2262,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2123
2262
  self.execute_command(update_cmd)
2124
2263
 
2125
2264
  def _clean_project(self, options: t.Any) -> None:
2265
+ assert self.code_cleaner is not None
2126
2266
  if options.clean:
2127
2267
  if self.pkg_dir:
2128
2268
  self.console.print("\n" + "-" * 80)
@@ -2142,6 +2282,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2142
2282
  self.code_cleaner.clean_files(tests_dir)
2143
2283
 
2144
2284
  async def _clean_project_async(self, options: t.Any) -> None:
2285
+ assert self.code_cleaner is not None
2145
2286
  if options.clean:
2146
2287
  if self.pkg_dir:
2147
2288
  self.console.print("\n" + "-" * 80)
@@ -2309,31 +2450,38 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2309
2450
  except (OSError, PermissionError):
2310
2451
  return "medium"
2311
2452
 
2453
+ def _calculate_test_metrics(self, test_files: list[Path]) -> tuple[int, int]:
2454
+ total_test_size = 0
2455
+ slow_tests = 0
2456
+ for test_file in test_files:
2457
+ try:
2458
+ size = test_file.stat().st_size
2459
+ total_test_size += size
2460
+ if size > 30_000 or "integration" in test_file.name.lower():
2461
+ slow_tests += 1
2462
+ except (OSError, PermissionError):
2463
+ continue
2464
+ return total_test_size, slow_tests
2465
+
2466
+ def _determine_test_complexity(
2467
+ self, test_count: int, avg_size: float, slow_ratio: float
2468
+ ) -> str:
2469
+ if test_count > 100 or avg_size > 25_000 or slow_ratio > 0.4:
2470
+ return "high"
2471
+ elif test_count > 50 or avg_size > 15_000 or slow_ratio > 0.2:
2472
+ return "medium"
2473
+ return "low"
2474
+
2312
2475
  def _analyze_test_workload(self) -> dict[str, t.Any]:
2313
2476
  try:
2314
2477
  test_files = self._get_cached_files_with_mtime("test_*.py")
2315
2478
  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
2479
+ total_test_size, slow_tests = self._calculate_test_metrics(test_files)
2326
2480
  avg_test_size = total_test_size / len(test_files) if test_files else 0
2327
2481
  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
-
2482
+ complexity = self._determine_test_complexity(
2483
+ len(test_files), avg_test_size, slow_test_ratio
2484
+ )
2337
2485
  return {
2338
2486
  "total_files": len(py_files),
2339
2487
  "test_files": len(test_files),
@@ -2354,8 +2502,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2354
2502
  return min(cpu_count // 3, 2)
2355
2503
  elif workload["complexity"] == "medium":
2356
2504
  return min(cpu_count // 2, 4)
2357
- else:
2358
- return min(cpu_count, 8)
2505
+ return min(cpu_count, 8)
2359
2506
 
2360
2507
  def _print_ai_agent_files(self, options: t.Any) -> None:
2361
2508
  if getattr(options, "ai_agent", False):
@@ -2422,12 +2569,20 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2422
2569
  def _bump_version(self, options: OptionsProtocol) -> None:
2423
2570
  for option in (options.publish, options.bump):
2424
2571
  if option:
2572
+ version_type = str(option)
2573
+ if self._has_version_been_bumped(version_type):
2574
+ self.console.print("\n" + "-" * 80)
2575
+ self.console.print(
2576
+ f"[bold yellow]📦 VERSION[/bold yellow] [bold bright_white]Version already bumped ({version_type}), skipping to avoid duplicate bump[/bold bright_white]"
2577
+ )
2578
+ self.console.print("-" * 80 + "\n")
2579
+ return
2425
2580
  self.console.print("\n" + "-" * 80)
2426
2581
  self.console.print(
2427
2582
  f"[bold bright_magenta]📦 VERSION[/bold bright_magenta] [bold bright_white]Bumping {option} version[/bold bright_white]"
2428
2583
  )
2429
2584
  self.console.print("-" * 80 + "\n")
2430
- if str(option) in ("minor", "major"):
2585
+ if version_type in ("minor", "major"):
2431
2586
  from rich.prompt import Confirm
2432
2587
 
2433
2588
  if not Confirm.ask(
@@ -2439,6 +2594,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2439
2594
  )
2440
2595
  return
2441
2596
  self.execute_command(["uv", "version", "--bump", option])
2597
+ self._mark_version_bumped(version_type)
2442
2598
  break
2443
2599
 
2444
2600
  def _publish_project(self, options: OptionsProtocol) -> None:
@@ -2458,7 +2614,18 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2458
2614
  "[bold bright_red]❌ Build failed. Please fix errors.[/bold bright_red]"
2459
2615
  )
2460
2616
  raise SystemExit(1)
2461
- self.execute_command(["uv", "publish"])
2617
+ try:
2618
+ self.execute_command(["uv", "publish"])
2619
+ self._mark_publish_completed()
2620
+ self._clear_state()
2621
+ self.console.print(
2622
+ "\n[bold bright_green]✅ Package published successfully![/bold bright_green]"
2623
+ )
2624
+ except SystemExit:
2625
+ self.console.print(
2626
+ "\n[bold bright_red]❌ Publish failed. Run crackerjack again to retry publishing without re-bumping version.[/bold bright_red]"
2627
+ )
2628
+ raise
2462
2629
 
2463
2630
  def _commit_and_push(self, options: OptionsProtocol) -> None:
2464
2631
  if options.commit:
@@ -2507,7 +2674,98 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2507
2674
  stderr.decode() if stderr else "",
2508
2675
  )
2509
2676
 
2677
+ def _run_comprehensive_quality_checks(self, options: OptionsProtocol) -> None:
2678
+ if options.skip_hooks or (
2679
+ options.test
2680
+ and not any([options.publish, options.bump, options.commit, options.all])
2681
+ ):
2682
+ return
2683
+ needs_comprehensive = any(
2684
+ [options.publish, options.bump, options.commit, options.all]
2685
+ )
2686
+ if not needs_comprehensive:
2687
+ return
2688
+ self.console.print("\n" + "-" * 80)
2689
+ self.console.print(
2690
+ "[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
2691
+ )
2692
+ self.console.print("-" * 80 + "\n")
2693
+ cmd = [
2694
+ "uv",
2695
+ "run",
2696
+ "pre-commit",
2697
+ "run",
2698
+ "--all-files",
2699
+ "--hook-stage=manual",
2700
+ "-c",
2701
+ ".pre-commit-config.yaml",
2702
+ ]
2703
+ result = self.execute_command(cmd)
2704
+ if result.returncode > 0:
2705
+ self.console.print(
2706
+ "\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
2707
+ )
2708
+ self.console.print(
2709
+ "\n[bold red]Cannot proceed with publishing/committing until all quality checks pass.[/bold red]\n"
2710
+ )
2711
+ raise SystemExit(1)
2712
+ else:
2713
+ self.console.print(
2714
+ "\n[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
2715
+ )
2716
+
2717
+ async def _run_comprehensive_quality_checks_async(
2718
+ self, options: OptionsProtocol
2719
+ ) -> None:
2720
+ if options.skip_hooks or (
2721
+ options.test
2722
+ and not any([options.publish, options.bump, options.commit, options.all])
2723
+ ):
2724
+ return
2725
+
2726
+ needs_comprehensive = any(
2727
+ [options.publish, options.bump, options.commit, options.all]
2728
+ )
2729
+
2730
+ if not needs_comprehensive:
2731
+ return
2732
+
2733
+ self.console.print("\n" + "-" * 80)
2734
+ self.console.print(
2735
+ "[bold bright_magenta]🔍 COMPREHENSIVE QUALITY[/bold bright_magenta] [bold bright_white]Running all quality checks before publish/commit[/bold bright_white]"
2736
+ )
2737
+ self.console.print("-" * 80 + "\n")
2738
+
2739
+ cmd = [
2740
+ "uv",
2741
+ "run",
2742
+ "pre-commit",
2743
+ "run",
2744
+ "--all-files",
2745
+ "--hook-stage=manual",
2746
+ "-c",
2747
+ ".pre-commit-config.yaml",
2748
+ ]
2749
+
2750
+ result = await self.execute_command_async(cmd)
2751
+
2752
+ if result.returncode > 0:
2753
+ self.console.print(
2754
+ "\n[bold bright_red]❌ Comprehensive quality checks failed![/bold bright_red]"
2755
+ )
2756
+ if result.stderr:
2757
+ self.console.print(f"[dim]Error details: {result.stderr}[/dim]")
2758
+ self.console.print(
2759
+ "\n[bold red]Cannot proceed with publishing/committing until all quality checks pass.[/bold red]\n"
2760
+ )
2761
+ raise SystemExit(1)
2762
+ else:
2763
+ self.console.print(
2764
+ "[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
2765
+ )
2766
+
2510
2767
  def process(self, options: OptionsProtocol) -> None:
2768
+ assert self.project_manager is not None
2511
2769
  self.console.print("\n" + "-" * 80)
2512
2770
  self.console.print(
2513
2771
  "[bold bright_cyan]⚒️ CRACKERJACKING[/bold bright_cyan] [bold bright_white]Starting workflow execution[/bold bright_white]"
@@ -2533,9 +2791,10 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2533
2791
  "\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
2534
2792
  )
2535
2793
  self._run_tests(options)
2794
+ self._run_comprehensive_quality_checks(options)
2536
2795
  self._bump_version(options)
2537
- self._publish_project(options)
2538
2796
  self._commit_and_push(options)
2797
+ self._publish_project(options)
2539
2798
  self.console.print("\n" + "-" * 80)
2540
2799
  self.console.print(
2541
2800
  "[bold bright_green]✨ CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
@@ -2543,6 +2802,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2543
2802
  self.console.print("-" * 80 + "\n")
2544
2803
 
2545
2804
  async def process_async(self, options: OptionsProtocol) -> None:
2805
+ assert self.project_manager is not None
2546
2806
  self.console.print("\n" + "-" * 80)
2547
2807
  self.console.print(
2548
2808
  "[bold bright_cyan]⚒️ CRACKERJACKING[/bold bright_cyan] [bold bright_white]Starting workflow execution (async optimized)[/bold bright_white]"
@@ -2568,9 +2828,10 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
2568
2828
  "\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
2569
2829
  )
2570
2830
  await self._run_tests_async(options)
2831
+ await self._run_comprehensive_quality_checks_async(options)
2571
2832
  self._bump_version(options)
2572
- self._publish_project(options)
2573
2833
  self._commit_and_push(options)
2834
+ self._publish_project(options)
2574
2835
  self.console.print("\n" + "-" * 80)
2575
2836
  self.console.print(
2576
2837
  "[bold bright_green]✨ CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
@@ -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,9 +109,13 @@ 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
- if isinstance(result, dict) and result.get("status") == HookStatus.SUCCESS:
114
+ if (
115
+ isinstance(result, dict)
116
+ and "status" in result
117
+ and result["status"] == HookStatus.SUCCESS
118
+ ):
115
119
  processed_results.append(success_handler(result))
116
120
  else:
117
121
  processed_results.append(failure_handler(result))
@@ -156,7 +160,7 @@ class EnhancedCommandRunner:
156
160
 
157
161
  def clean_python_code(code: str) -> str:
158
162
  lines = code.splitlines()
159
- cleaned_lines = []
163
+ cleaned_lines: list[str] = []
160
164
  for line in lines:
161
165
  match line.strip():
162
166
  case "":
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.27.0"
7
+ version = "0.27.2"
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]
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.26.0"
7
+ version = "0.27.3"
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.3"
118
118
  source = { editable = "." }
119
119
  dependencies = [
120
120
  { name = "aiofiles" },
File without changes
File without changes
File without changes