crackerjack 0.21.5__tar.gz → 0.21.7__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.
Files changed (90) hide show
  1. {crackerjack-0.21.5 → crackerjack-0.21.7}/PKG-INFO +8 -8
  2. {crackerjack-0.21.5 → crackerjack-0.21.7}/README.md +7 -7
  3. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.gitignore +4 -1
  4. crackerjack-0.21.7/crackerjack/.ruff_cache/0.12.1/5056746222905752453 +0 -0
  5. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/__init__.py +0 -1
  6. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/__main__.py +0 -1
  7. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/crackerjack.py +100 -46
  8. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/errors.py +0 -1
  9. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/interactive.py +0 -10
  10. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/pyproject.toml +1 -1
  11. {crackerjack-0.21.5 → crackerjack-0.21.7}/pyproject.toml +1 -1
  12. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/conftest.py +0 -1
  13. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_crackerjack.py +38 -25
  14. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_crackerjack_runner.py +0 -1
  15. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_errors.py +0 -1
  16. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_interactive.py +0 -1
  17. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_interactive_run.py +0 -1
  18. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_main.py +0 -3
  19. crackerjack-0.21.7/tests/test_multiline_functions.py +163 -0
  20. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_py313_advanced.py +0 -1
  21. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_py313_features.py +0 -1
  22. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_pytest_features.py +0 -1
  23. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/test_structured_errors.py +0 -1
  24. crackerjack-0.21.5/crackerjack/.ruff_cache/0.12.1/5056746222905752453 +0 -0
  25. {crackerjack-0.21.5 → crackerjack-0.21.7}/LICENSE +0 -0
  26. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.libcst.codemod.yaml +0 -0
  27. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pdm.toml +0 -0
  28. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pre-commit-config-ai.yaml +0 -0
  29. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pre-commit-config.yaml +0 -0
  30. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pytest_cache/.gitignore +0 -0
  31. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pytest_cache/CACHEDIR.TAG +0 -0
  32. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pytest_cache/README.md +0 -0
  33. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pytest_cache/v/cache/nodeids +0 -0
  34. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.pytest_cache/v/cache/stepwise +0 -0
  35. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/.gitignore +0 -0
  36. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
  37. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
  38. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
  39. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
  40. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
  41. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
  42. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.11/18187162184424859798 +0 -0
  43. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/16869036553936192448 +0 -0
  44. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
  45. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
  46. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/4441409093023629623 +0 -0
  47. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
  48. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.13/4240757255861806333 +0 -0
  49. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
  50. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
  51. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
  52. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
  53. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
  54. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
  55. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
  56. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.12.0/5056746222905752453 +0 -0
  57. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
  58. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
  59. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
  60. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
  61. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
  62. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
  63. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
  64. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
  65. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
  66. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
  67. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
  68. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
  69. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
  70. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
  71. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
  72. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
  73. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
  74. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
  75. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
  76. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
  77. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
  78. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
  79. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
  80. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
  81. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
  82. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
  83. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/.ruff_cache/CACHEDIR.TAG +0 -0
  84. {crackerjack-0.21.5 → crackerjack-0.21.7}/crackerjack/py313.py +0 -0
  85. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/TESTING.md +0 -0
  86. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/__init__.py +0 -0
  87. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/data/comments_sample.txt +0 -0
  88. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/data/docstrings_sample.txt +0 -0
  89. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/data/expected_comments_sample.txt +0 -0
  90. {crackerjack-0.21.5 → crackerjack-0.21.7}/tests/data/init.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crackerjack
3
- Version: 0.21.5
3
+ Version: 0.21.7
4
4
  Summary: Crackerjack: code quality toolkit
5
5
  Keywords: bandit,black,creosote,mypy,pyright,pytest,refurb,ruff
6
6
  Author-Email: lesleslie <les@wedgwoodwebworks.com>
@@ -102,7 +102,7 @@ If you're new to Crackerjack, follow these steps:
102
102
 
103
103
  Or use the interactive Rich UI:
104
104
  ```
105
- python -m crackerjack --rich-ui
105
+ python -m crackerjack --interactive
106
106
  ```
107
107
 
108
108
  ---
@@ -269,7 +269,7 @@ python -m crackerjack -t --benchmark-regression --benchmark-regression-threshold
269
269
 
270
270
  Or with the interactive Rich UI:
271
271
  ```
272
- python -m crackerjack --rich-ui
272
+ python -m crackerjack --interactive
273
273
  ```
274
274
 
275
275
  ## Usage
@@ -361,7 +361,7 @@ runner.process(MyOptions())
361
361
  - `--benchmark-regression`: Fail tests if benchmarks regress beyond threshold.
362
362
  - `--benchmark-regression-threshold`: Set threshold percentage for benchmark regression (default 5.0%).
363
363
  - `-a`, `--all`: Run with `-x -t -p <micro|minor|major> -c` development options.
364
- - `--rich-ui`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
364
+ - `--interactive`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
365
365
  - `--ai-agent`: Enable AI agent mode with structured output (see [AI Agent Integration](#ai-agent-integration)).
366
366
  - `--help`: Display help.
367
367
 
@@ -461,7 +461,7 @@ runner.process(MyOptions())
461
461
 
462
462
  - **Rich Interactive Mode** - Run with the interactive Rich UI:
463
463
  ```bash
464
- python -m crackerjack --rich-ui
464
+ python -m crackerjack --interactive
465
465
  ```
466
466
 
467
467
  - **AI Integration** - Run with structured output for AI tools:
@@ -500,10 +500,10 @@ Crackerjack now offers an enhanced interactive experience through its Rich UI:
500
500
  - **Error Visualization:** Errors are presented in a structured, easy-to-understand format with recovery suggestions
501
501
  - **File Selection:** Interactive file browser for operations that require selecting files
502
502
 
503
- To use the Rich UI, run Crackerjack with the `--rich-ui` flag:
503
+ To use the Rich UI, run Crackerjack with the `--interactive` flag:
504
504
 
505
505
  ```bash
506
- python -m crackerjack --rich-ui
506
+ python -m crackerjack --interactive
507
507
  ```
508
508
 
509
509
  This launches an interactive terminal interface where you can:
@@ -542,7 +542,7 @@ python -m crackerjack -v
542
542
  For the most comprehensive error details with visual formatting, combine verbose mode with the Rich UI:
543
543
 
544
544
  ```bash
545
- python -m crackerjack --rich-ui -v
545
+ python -m crackerjack --interactive -v
546
546
  ```
547
547
 
548
548
  ## Python 3.13+ Features
@@ -58,7 +58,7 @@ If you're new to Crackerjack, follow these steps:
58
58
 
59
59
  Or use the interactive Rich UI:
60
60
  ```
61
- python -m crackerjack --rich-ui
61
+ python -m crackerjack --interactive
62
62
  ```
63
63
 
64
64
  ---
@@ -225,7 +225,7 @@ python -m crackerjack -t --benchmark-regression --benchmark-regression-threshold
225
225
 
226
226
  Or with the interactive Rich UI:
227
227
  ```
228
- python -m crackerjack --rich-ui
228
+ python -m crackerjack --interactive
229
229
  ```
230
230
 
231
231
  ## Usage
@@ -317,7 +317,7 @@ runner.process(MyOptions())
317
317
  - `--benchmark-regression`: Fail tests if benchmarks regress beyond threshold.
318
318
  - `--benchmark-regression-threshold`: Set threshold percentage for benchmark regression (default 5.0%).
319
319
  - `-a`, `--all`: Run with `-x -t -p <micro|minor|major> -c` development options.
320
- - `--rich-ui`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
320
+ - `--interactive`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
321
321
  - `--ai-agent`: Enable AI agent mode with structured output (see [AI Agent Integration](#ai-agent-integration)).
322
322
  - `--help`: Display help.
323
323
 
@@ -417,7 +417,7 @@ runner.process(MyOptions())
417
417
 
418
418
  - **Rich Interactive Mode** - Run with the interactive Rich UI:
419
419
  ```bash
420
- python -m crackerjack --rich-ui
420
+ python -m crackerjack --interactive
421
421
  ```
422
422
 
423
423
  - **AI Integration** - Run with structured output for AI tools:
@@ -456,10 +456,10 @@ Crackerjack now offers an enhanced interactive experience through its Rich UI:
456
456
  - **Error Visualization:** Errors are presented in a structured, easy-to-understand format with recovery suggestions
457
457
  - **File Selection:** Interactive file browser for operations that require selecting files
458
458
 
459
- To use the Rich UI, run Crackerjack with the `--rich-ui` flag:
459
+ To use the Rich UI, run Crackerjack with the `--interactive` flag:
460
460
 
461
461
  ```bash
462
- python -m crackerjack --rich-ui
462
+ python -m crackerjack --interactive
463
463
  ```
464
464
 
465
465
  This launches an interactive terminal interface where you can:
@@ -498,7 +498,7 @@ python -m crackerjack -v
498
498
  For the most comprehensive error details with visual formatting, combine verbose mode with the Rich UI:
499
499
 
500
500
  ```bash
501
- python -m crackerjack --rich-ui -v
501
+ python -m crackerjack --interactive -v
502
502
  ```
503
503
 
504
504
  ## Python 3.13+ Features
@@ -13,5 +13,8 @@
13
13
  /scratch/
14
14
  /.zencoder/
15
15
  /.benchmarks/
16
-
17
16
  **/.claude/settings.local.json
17
+ /complexipy.json
18
+ /coverage.json
19
+ /test-results.xml
20
+ /.coverage
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
-
3
2
  from .crackerjack import Crackerjack, create_crackerjack_runner
4
3
  from .errors import (
5
4
  CleaningError,
@@ -1,5 +1,4 @@
1
1
  from enum import Enum
2
-
3
2
  import typer
4
3
  from pydantic import BaseModel, field_validator
5
4
  from rich.console import Console
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from subprocess import CompletedProcess
7
7
  from subprocess import run as execute
8
8
  from tomllib import loads
9
-
10
9
  from pydantic import BaseModel
11
10
  from rich.console import Console
12
11
  from tomli_w import dumps
@@ -86,28 +85,93 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
86
85
  except Exception as e:
87
86
  print(f"Error cleaning {file_path}: {e}")
88
87
 
88
+ def _initialize_docstring_state(self) -> dict[str, t.Any]:
89
+ return {
90
+ "in_docstring": False,
91
+ "delimiter": None,
92
+ "waiting": False,
93
+ "function_indent": 0,
94
+ "removed_docstring": False,
95
+ "in_multiline_def": False,
96
+ }
97
+
98
+ def _handle_function_definition(
99
+ self, line: str, stripped: str, state: dict[str, t.Any]
100
+ ) -> bool:
101
+ if self._is_function_or_class_definition(stripped):
102
+ state["waiting"] = True
103
+ state["function_indent"] = len(line) - len(line.lstrip())
104
+ state["removed_docstring"] = False
105
+ state["in_multiline_def"] = not stripped.endswith(":")
106
+ return True
107
+ return False
108
+
109
+ def _handle_multiline_definition(
110
+ self, line: str, stripped: str, state: dict[str, t.Any]
111
+ ) -> bool:
112
+ if state["in_multiline_def"]:
113
+ if stripped.endswith(":"):
114
+ state["in_multiline_def"] = False
115
+ return True
116
+ return False
117
+
118
+ def _handle_waiting_docstring(
119
+ self, lines: list[str], i: int, stripped: str, state: dict[str, t.Any]
120
+ ) -> tuple[bool, str | None]:
121
+ if state["waiting"] and stripped:
122
+ if self._handle_docstring_start(stripped, state):
123
+ pass_line = None
124
+ if not state["in_docstring"]:
125
+ function_indent: int = state["function_indent"]
126
+ if self._needs_pass_statement(lines, i + 1, function_indent):
127
+ pass_line = " " * (function_indent + 4) + "pass"
128
+ state["removed_docstring"] = True
129
+ return True, pass_line
130
+ else:
131
+ state["waiting"] = False
132
+ return False, None
133
+
134
+ def _handle_docstring_content(
135
+ self, lines: list[str], i: int, stripped: str, state: dict[str, t.Any]
136
+ ) -> tuple[bool, str | None]:
137
+ if state["in_docstring"]:
138
+ if self._handle_docstring_end(stripped, state):
139
+ pass_line = None
140
+ function_indent: int = state["function_indent"]
141
+ if self._needs_pass_statement(lines, i + 1, function_indent):
142
+ pass_line = " " * (function_indent + 4) + "pass"
143
+ state["removed_docstring"] = False
144
+ return True, pass_line
145
+ else:
146
+ return True, None
147
+ return False, None
148
+
149
+ def _process_line(
150
+ self, lines: list[str], i: int, line: str, state: dict[str, t.Any]
151
+ ) -> tuple[bool, str | None]:
152
+ stripped = line.strip()
153
+ if self._handle_function_definition(line, stripped, state):
154
+ return True, line
155
+ if self._handle_multiline_definition(line, stripped, state):
156
+ return True, line
157
+ handled, pass_line = self._handle_waiting_docstring(lines, i, stripped, state)
158
+ if handled:
159
+ return True, pass_line
160
+ handled, pass_line = self._handle_docstring_content(lines, i, stripped, state)
161
+ if handled:
162
+ return True, pass_line
163
+ if state["removed_docstring"] and stripped:
164
+ state["removed_docstring"] = False
165
+ return False, line
166
+
89
167
  def remove_docstrings(self, code: str) -> str:
90
168
  lines = code.split("\n")
91
169
  cleaned_lines = []
92
- docstring_state = {"in_docstring": False, "delimiter": None, "waiting": False}
93
- for line in lines:
94
- stripped = line.strip()
95
- if self._is_function_or_class_definition(stripped):
96
- docstring_state["waiting"] = True
97
- cleaned_lines.append(line)
98
- continue
99
- if docstring_state["waiting"] and stripped:
100
- if self._handle_docstring_start(stripped, docstring_state):
101
- continue
102
- else:
103
- docstring_state["waiting"] = False
104
- if docstring_state["in_docstring"]:
105
- if self._handle_docstring_end(stripped, docstring_state):
106
- continue
107
- else:
108
- continue
109
- cleaned_lines.append(line)
110
-
170
+ docstring_state = self._initialize_docstring_state()
171
+ for i, line in enumerate(lines):
172
+ _, result_line = self._process_line(lines, i, line, docstring_state)
173
+ if result_line:
174
+ cleaned_lines.append(result_line)
111
175
  return "\n".join(cleaned_lines)
112
176
 
113
177
  def _is_function_or_class_definition(self, stripped_line: str) -> bool:
@@ -137,6 +201,21 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
137
201
  state["in_docstring"] = False
138
202
  state["delimiter"] = None
139
203
  return True
204
+ return False
205
+
206
+ def _needs_pass_statement(
207
+ self, lines: list[str], start_index: int, function_indent: int
208
+ ) -> bool:
209
+ for i in range(start_index, len(lines)):
210
+ line = lines[i]
211
+ stripped = line.strip()
212
+ if not stripped:
213
+ continue
214
+ line_indent = len(line) - len(line.lstrip())
215
+ if line_indent <= function_indent:
216
+ return True
217
+ if line_indent > function_indent:
218
+ return False
140
219
  return True
141
220
 
142
221
  def remove_line_comments(self, code: str) -> str:
@@ -149,7 +228,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
149
228
  cleaned_line = self._process_line_for_comments(line)
150
229
  if cleaned_line or not line.strip():
151
230
  cleaned_lines.append(cleaned_line or line)
152
-
153
231
  return "\n".join(cleaned_lines)
154
232
 
155
233
  def _process_line_for_comments(self, line: str) -> str:
@@ -162,7 +240,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
162
240
  break
163
241
  else:
164
242
  result.append(char)
165
-
166
243
  return "".join(result).rstrip()
167
244
 
168
245
  def _handle_string_character(
@@ -173,18 +250,14 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
173
250
  string_state: dict[str, t.Any],
174
251
  result: list[str],
175
252
  ) -> bool:
176
- """Handle string quote characters. Returns True if character was handled."""
177
253
  if char not in ("'", '"'):
178
254
  return False
179
-
180
255
  if index > 0 and line[index - 1] == "\\":
181
256
  return False
182
-
183
257
  if string_state["in_string"] is None:
184
258
  string_state["in_string"] = char
185
259
  elif string_state["in_string"] == char:
186
260
  string_state["in_string"] = None
187
-
188
261
  result.append(char)
189
262
  return True
190
263
 
@@ -196,14 +269,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
196
269
  string_state: dict[str, t.Any],
197
270
  result: list[str],
198
271
  ) -> bool:
199
- """Handle comment character. Returns True if comment was found."""
200
272
  if char != "#" or string_state["in_string"] is not None:
201
273
  return False
202
-
203
274
  comment = line[index:].strip()
204
275
  if self._is_special_comment_line(comment):
205
276
  result.append(line[index:])
206
-
207
277
  return True
208
278
 
209
279
  def _is_special_comment_line(self, comment: str) -> bool:
@@ -227,13 +297,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
227
297
  ):
228
298
  continue
229
299
  cleaned_lines.append(line)
230
-
231
300
  return "\n".join(self._remove_trailing_empty_lines(cleaned_lines))
232
301
 
233
302
  def _update_function_state(
234
303
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
235
304
  ) -> None:
236
- """Update function tracking state based on current line."""
237
305
  if stripped_line.startswith(("def ", "async def ")):
238
306
  function_tracker["in_function"] = True
239
307
  function_tracker["function_indent"] = len(line) - len(stripped_line)
@@ -244,7 +312,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
244
312
  def _is_function_end(
245
313
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
246
314
  ) -> bool:
247
- """Check if current line marks the end of a function."""
248
315
  return (
249
316
  function_tracker["in_function"]
250
317
  and bool(line)
@@ -259,13 +326,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
259
326
  cleaned_lines: list[str],
260
327
  function_tracker: dict[str, t.Any],
261
328
  ) -> bool:
262
- """Determine if an empty line should be skipped."""
263
329
  if line_idx > 0 and cleaned_lines and (not cleaned_lines[-1]):
264
330
  return True
265
-
266
331
  if function_tracker["in_function"]:
267
332
  return self._should_skip_function_empty_line(line_idx, lines)
268
-
269
333
  return False
270
334
 
271
335
  def _should_skip_function_empty_line(self, line_idx: int, lines: list[str]) -> bool:
@@ -280,7 +344,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
280
344
  return True
281
345
  if next_line in ("pass", "break", "continue", "raise"):
282
346
  return True
283
-
284
347
  return self._is_special_comment(next_line)
285
348
 
286
349
  def _is_special_comment(self, line: str) -> bool:
@@ -420,15 +483,12 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
420
483
  for tool, settings in our_toml_config.get("tool", {}).items():
421
484
  if tool not in pkg_toml_config["tool"]:
422
485
  pkg_toml_config["tool"][tool] = {}
423
-
424
486
  pkg_tool_config = pkg_toml_config["tool"][tool]
425
-
426
487
  self._merge_tool_config(settings, pkg_tool_config, tool)
427
488
 
428
489
  def _merge_tool_config(
429
490
  self, our_config: dict[str, t.Any], pkg_config: dict[str, t.Any], tool: str
430
491
  ) -> None:
431
- """Recursively merge tool configuration, preserving existing project settings."""
432
492
  for setting, value in our_config.items():
433
493
  if isinstance(value, dict):
434
494
  self._merge_nested_config(setting, value, pkg_config)
@@ -438,21 +498,17 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
438
498
  def _merge_nested_config(
439
499
  self, setting: str, value: dict[str, t.Any], pkg_config: dict[str, t.Any]
440
500
  ) -> None:
441
- """Handle nested configuration merging."""
442
501
  if setting not in pkg_config:
443
502
  pkg_config[setting] = {}
444
503
  elif not isinstance(pkg_config[setting], dict):
445
504
  pkg_config[setting] = {}
446
-
447
505
  self._merge_tool_config(value, pkg_config[setting], "")
448
-
449
506
  for k, v in value.items():
450
507
  self._merge_nested_value(k, v, pkg_config[setting])
451
508
 
452
509
  def _merge_nested_value(
453
510
  self, key: str, value: t.Any, nested_config: dict[str, t.Any]
454
511
  ) -> None:
455
- """Merge individual nested values."""
456
512
  if isinstance(value, str | list) and "crackerjack" in str(value):
457
513
  nested_config[key] = self.swap_package_name(value)
458
514
  elif self._is_mergeable_list(key, value):
@@ -467,7 +523,6 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
467
523
  def _merge_direct_config(
468
524
  self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
469
525
  ) -> None:
470
- """Handle direct configuration merging."""
471
526
  if isinstance(value, str | list) and "crackerjack" in str(value):
472
527
  pkg_config[setting] = self.swap_package_name(value)
473
528
  elif self._is_mergeable_list(setting, value):
@@ -633,7 +688,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
633
688
  )
634
689
  if result.returncode == 0:
635
690
  self.console.print("PDM installed: ✅\n")
636
- self.execute_command(["pdm", "sync"])
691
+ self.execute_command(["pdm", "lock"])
637
692
  self.console.print("Lock file updated: ✅\n")
638
693
  else:
639
694
  self.console.print(
@@ -730,7 +785,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
730
785
  self._add_benchmark_flags(test, options)
731
786
  else:
732
787
  self._add_worker_flags(test, options, project_size)
733
-
734
788
  return test
735
789
 
736
790
  def _detect_project_size(self) -> str:
@@ -2,7 +2,6 @@ import sys
2
2
  import typing as t
3
3
  from enum import Enum
4
4
  from pathlib import Path
5
-
6
5
  from rich.console import Console
7
6
  from rich.panel import Panel
8
7
 
@@ -2,7 +2,6 @@ import time
2
2
  import typing as t
3
3
  from enum import Enum, auto
4
4
  from pathlib import Path
5
-
6
5
  from rich.box import ROUNDED
7
6
  from rich.console import Console
8
7
  from rich.layout import Layout
@@ -19,7 +18,6 @@ from rich.prompt import Confirm, Prompt
19
18
  from rich.table import Table
20
19
  from rich.text import Text
21
20
  from rich.tree import Tree
22
-
23
21
  from .errors import CrackerjackError, ErrorCode, handle_error
24
22
 
25
23
 
@@ -289,7 +287,6 @@ class InteractiveCLI:
289
287
  )
290
288
  total_tasks = len(self.workflow.tasks)
291
289
  progress_task = progress.add_task("Running workflow", total=total_tasks)
292
-
293
290
  return {
294
291
  "progress": progress,
295
292
  "progress_task": progress_task,
@@ -299,14 +296,11 @@ class InteractiveCLI:
299
296
  def _execute_workflow_loop(
300
297
  self, layout: Layout, progress_tracker: dict[str, t.Any], live: Live
301
298
  ) -> None:
302
- """Execute the main workflow loop."""
303
299
  while not self.workflow.all_tasks_completed():
304
300
  layout["tasks"].update(self.show_task_table())
305
301
  next_task = self.workflow.get_next_task()
306
-
307
302
  if not next_task:
308
303
  break
309
-
310
304
  if self._should_execute_task(layout, next_task, live):
311
305
  self._execute_task(layout, next_task, progress_tracker)
312
306
  else:
@@ -322,20 +316,16 @@ class InteractiveCLI:
322
316
  def _execute_task(
323
317
  self, layout: Layout, task: Task, progress_tracker: dict[str, t.Any]
324
318
  ) -> None:
325
- """Execute a single task and update progress."""
326
319
  task.start()
327
320
  layout["details"].update(self.show_task_status(task))
328
321
  time.sleep(1)
329
-
330
322
  success = self._simulate_task_execution()
331
-
332
323
  if success:
333
324
  task.complete()
334
325
  progress_tracker["completed_tasks"] += 1
335
326
  else:
336
327
  error = self._create_task_error(task.name)
337
328
  task.fail(error)
338
-
339
329
  progress_tracker["progress"].update(
340
330
  progress_tracker["progress_task"],
341
331
  completed=progress_tracker["completed_tasks"],
@@ -4,7 +4,7 @@ requires = [ "pdm-backend" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.21.4"
7
+ version = "0.21.6"
8
8
  description = "Crackerjack: code quality toolkit"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "crackerjack"
9
- version = "0.21.5"
9
+ version = "0.21.7"
10
10
  description = "Crackerjack: code quality toolkit"
11
11
  readme = "README.md"
12
12
  keywords = [
@@ -2,7 +2,6 @@ import os
2
2
  import time
3
3
  import typing as t
4
4
  from pathlib import Path
5
-
6
5
  import pytest
7
6
  from pytest import Config, Item, Parser
8
7
 
@@ -6,7 +6,6 @@ from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
8
  from unittest.mock import MagicMock, patch
9
-
10
9
  import pytest
11
10
  from rich.console import Console
12
11
  from crackerjack.crackerjack import (
@@ -317,12 +316,10 @@ class TestCrackerjackProcess:
317
316
  ) -> None:
318
317
  options = options_factory(bump="minor", no_config_updates=True)
319
318
  cj = Crackerjack(dry_run=True)
320
-
321
319
  with patch("rich.prompt.Confirm.ask", return_value=True) as mock_confirm:
322
320
  with patch.object(Crackerjack, "execute_command") as mock_exec:
323
321
  mock_exec.return_value = MagicMock(returncode=0)
324
322
  cj._bump_version(options)
325
-
326
323
  mock_confirm.assert_called_once_with(
327
324
  "Are you sure you want to bump the minor version?", default=False
328
325
  )
@@ -339,12 +336,10 @@ class TestCrackerjackProcess:
339
336
  ) -> None:
340
337
  options = options_factory(bump="minor", no_config_updates=True)
341
338
  cj = Crackerjack(dry_run=True)
342
-
343
339
  with patch("rich.prompt.Confirm.ask", return_value=False) as mock_confirm:
344
340
  with patch.object(Crackerjack, "execute_command") as mock_exec:
345
341
  mock_exec.return_value = MagicMock(returncode=0)
346
342
  cj._bump_version(options)
347
-
348
343
  mock_confirm.assert_called_once_with(
349
344
  "Are you sure you want to bump the minor version?", default=False
350
345
  )
@@ -361,12 +356,10 @@ class TestCrackerjackProcess:
361
356
  ) -> None:
362
357
  options = options_factory(bump="major", no_config_updates=True)
363
358
  cj = Crackerjack(dry_run=True)
364
-
365
359
  with patch("rich.prompt.Confirm.ask", return_value=True) as mock_confirm:
366
360
  with patch.object(Crackerjack, "execute_command") as mock_exec:
367
361
  mock_exec.return_value = MagicMock(returncode=0)
368
362
  cj._bump_version(options)
369
-
370
363
  mock_confirm.assert_called_once_with(
371
364
  "Are you sure you want to bump the major version?", default=False
372
365
  )
@@ -383,12 +376,10 @@ class TestCrackerjackProcess:
383
376
  ) -> None:
384
377
  options = options_factory(bump="micro", no_config_updates=True)
385
378
  cj = Crackerjack(dry_run=True)
386
-
387
379
  with patch("rich.prompt.Confirm.ask") as mock_confirm:
388
380
  with patch.object(Crackerjack, "execute_command") as mock_exec:
389
381
  mock_exec.return_value = MagicMock(returncode=0)
390
382
  cj._bump_version(options)
391
-
392
383
  mock_confirm.assert_not_called()
393
384
  mock_exec.assert_called_once_with(["pdm", "bump", "micro"])
394
385
 
@@ -405,7 +396,6 @@ class TestCrackerjackProcess:
405
396
  pytest_command = (_cj := Crackerjack(dry_run=True))._prepare_pytest_command(
406
397
  options
407
398
  )
408
-
409
399
  assert "--junitxml=test-results.xml" in pytest_command
410
400
  assert "--cov-report=json:coverage.json" in pytest_command
411
401
  assert "--quiet" in pytest_command
@@ -427,7 +417,6 @@ class TestCrackerjackProcess:
427
417
  pytest_command = (_cj := Crackerjack(dry_run=True))._prepare_pytest_command(
428
418
  options
429
419
  )
430
-
431
420
  assert "--junitxml=test-results.xml" in pytest_command
432
421
  assert "--cov-report=json:coverage.json" in pytest_command
433
422
  assert "--benchmark-json=benchmark.json" in pytest_command
@@ -447,12 +436,10 @@ class TestCrackerjackProcess:
447
436
  pytest_command = (_cj := Crackerjack(dry_run=True))._prepare_pytest_command(
448
437
  options
449
438
  )
450
-
451
439
  assert "--junitxml=test-results.xml" not in pytest_command
452
440
  assert "--cov-report=json:coverage.json" not in pytest_command
453
441
  assert "--benchmark-json=benchmark.json" not in pytest_command
454
442
  assert "--quiet" not in pytest_command
455
-
456
443
  assert "--capture=fd" in pytest_command
457
444
  assert "--disable-warnings" in pytest_command
458
445
  assert "--durations=0" in pytest_command
@@ -943,16 +930,11 @@ class TestCrackerjackProcess:
943
930
  options_factory: t.Callable[..., OptionsForTesting],
944
931
  ) -> None:
945
932
  mock_project = MagicMock()
946
-
947
933
  options = options_factory(ai_agent=True)
948
934
  mock_project.options = options
949
-
950
935
  mock_project.console = MagicMock()
951
-
952
936
  mock_project.execute_command.return_value = MagicMock(returncode=0)
953
-
954
937
  ProjectManager.run_pre_commit(mock_project)
955
-
956
938
  mock_project.execute_command.assert_called_with(
957
939
  ["pre-commit", "run", "--all-files", "-c", ".pre-commit-config-ai.yaml"]
958
940
  )
@@ -964,24 +946,17 @@ class TestCrackerjackProcess:
964
946
  options_factory: t.Callable[..., OptionsForTesting],
965
947
  ) -> None:
966
948
  mock_project = MagicMock(spec=ProjectManager)
967
-
968
949
  options = options_factory(ai_agent=True)
969
950
  mock_project.options = options
970
-
971
951
  mock_project.console = MagicMock()
972
-
973
952
  mock_project.config_manager = MagicMock()
974
-
975
953
  with patch.object(mock_project, "execute_command") as mock_execute:
976
954
  mock_execute.return_value = MagicMock(
977
955
  returncode=0,
978
956
  stdout="package1\npackage2\n",
979
957
  )
980
-
981
958
  original_method = ProjectManager.update_pkg_configs
982
-
983
959
  original_method(mock_project)
984
-
985
960
  mock_execute.assert_any_call(
986
961
  ["pre-commit", "install", "-c", ".pre-commit-config-ai.yaml"]
987
962
  )
@@ -1091,6 +1066,7 @@ class TestCrackerjackProcess:
1091
1066
  mock_clean_file.assert_called_once_with(py_file)
1092
1067
 
1093
1068
  def test_code_cleaner_remove_docstrings(self) -> None:
1069
+ import ast
1094
1070
  from rich.console import Console
1095
1071
  from crackerjack.crackerjack import CodeCleaner
1096
1072
 
@@ -1108,6 +1084,43 @@ class TestCrackerjackProcess:
1108
1084
  assert "This is a multi-line docstring." not in cleaned_code, (
1109
1085
  f"Got: {cleaned_code!r}"
1110
1086
  )
1087
+ try:
1088
+ ast.parse(cleaned_code)
1089
+ except SyntaxError as e:
1090
+ raise AssertionError(
1091
+ f"Cleaned code is not valid Python syntax: {e}\nCode: {cleaned_code!r}"
1092
+ )
1093
+
1094
+ def test_code_cleaner_remove_docstrings_empty_functions(self) -> None:
1095
+ import ast
1096
+ from rich.console import Console
1097
+ from crackerjack.crackerjack import CodeCleaner
1098
+
1099
+ code_cleaner = CodeCleaner(console=Console())
1100
+ test_code = """
1101
+ def empty_function():
1102
+ pass
1103
+ class TestClass:
1104
+ def method_with_docstring_only(self):
1105
+ pass
1106
+ def method_with_code(self):
1107
+ return True
1108
+ """
1109
+ cleaned_code = code_cleaner.remove_docstrings(test_code)
1110
+ print(f"Cleaned code: {cleaned_code!r}")
1111
+ assert '"""This function has only a docstring."""' not in cleaned_code
1112
+ assert '"""Class docstring."""' not in cleaned_code
1113
+ assert '"""Method with only docstring."""' not in cleaned_code
1114
+ assert '"""This method has code after docstring."""' not in cleaned_code
1115
+ assert "def empty_function():\n pass" in cleaned_code
1116
+ assert "def method_with_docstring_only(self):\n pass" in cleaned_code
1117
+ assert "def method_with_code(self):\n return True" in cleaned_code
1118
+ try:
1119
+ ast.parse(cleaned_code)
1120
+ except SyntaxError as e:
1121
+ raise AssertionError(
1122
+ f"Cleaned code is not valid Python syntax: {e}\nCode: {cleaned_code!r}"
1123
+ )
1111
1124
 
1112
1125
  def test_code_cleaner_remove_line_comments(self) -> None:
1113
1126
  from pathlib import Path
@@ -2,7 +2,6 @@ import tempfile
2
2
  import typing as t
3
3
  from pathlib import Path
4
4
  from unittest.mock import MagicMock, patch
5
-
6
5
  import pytest
7
6
  from rich.console import Console
8
7
  from crackerjack import create_crackerjack_runner
@@ -1,6 +1,5 @@
1
1
  import typing as t
2
2
  from unittest.mock import MagicMock, patch
3
-
4
3
  import pytest
5
4
  from rich.console import Console
6
5
  from crackerjack.errors import (
@@ -2,7 +2,6 @@ import io
2
2
  import time
3
3
  from pathlib import Path
4
4
  from unittest.mock import MagicMock, patch
5
-
6
5
  import pytest
7
6
  from rich.console import Console
8
7
  from rich.panel import Panel
@@ -1,5 +1,4 @@
1
1
  from unittest.mock import MagicMock, patch
2
-
3
2
  import pytest
4
3
  from rich.console import Console
5
4
  from crackerjack.errors import ErrorCode, ExecutionError
@@ -1,6 +1,5 @@
1
1
  import typing as t
2
2
  from unittest.mock import MagicMock, patch
3
-
4
3
  import pytest
5
4
  from typer.testing import CliRunner
6
5
  from crackerjack.__main__ import BumpOption, Options, app
@@ -59,7 +58,6 @@ def test_interactive_option(
59
58
  assert result.exit_code == 0
60
59
  mock_interactive.assert_called_once()
61
60
  mock_crackerjack_process.process.assert_not_called()
62
-
63
61
  mock_interactive.reset_mock()
64
62
  result = runner.invoke(app, ["--interactive"])
65
63
  assert result.exit_code == 0
@@ -203,7 +201,6 @@ def test_multiple_options(
203
201
  assert result.exit_code == 0
204
202
  mock_interactive.assert_called_once()
205
203
  mock_crackerjack_process.process.assert_not_called()
206
-
207
204
  result = runner.invoke(app, ["-c", "-d", "-t", "-x"])
208
205
  assert result.exit_code == 0
209
206
  mock_crackerjack_process.process.assert_called_once()
@@ -0,0 +1,163 @@
1
+ """Test cases for multiline function definitions in docstring removal."""
2
+
3
+ import ast
4
+ from rich.console import Console
5
+ from crackerjack.crackerjack import CodeCleaner
6
+
7
+
8
+ class TestMultilineFunctions:
9
+ def setup_method(self) -> None:
10
+ self.console = Console()
11
+ self.cleaner = CodeCleaner(console=self.console)
12
+
13
+ def test_multiline_async_function_with_docstring(self) -> None:
14
+ code = """class BackgroundTaskManager:
15
+ async def __aexit__(
16
+ self,
17
+ exc_type: type[BaseException] | None,
18
+ exc_val: BaseException | None,
19
+ exc_tb: t.Any | None,
20
+ ) -> None:
21
+ del exc_type, exc_val, exc_tb
22
+ self.logger.debug("Background task manager shutting down")
23
+ await wait_for_background_tasks(self.cleanup_timeout)
24
+ """
25
+ result = self.cleaner.remove_docstrings(code)
26
+ ast.parse(result)
27
+ assert '"""Clean up background tasks on exit."""' not in result
28
+ assert "del exc_type, exc_val, exc_tb" in result
29
+ assert "self.logger.debug" in result
30
+
31
+ def test_multiline_function_with_docstring(self) -> None:
32
+ code = '''def complex_function(
33
+ param1: str,
34
+ param2: int,
35
+ param3: dict[str, Any],
36
+ ) -> bool:
37
+ """This is a complex function with multiple parameters."""
38
+ return True
39
+ '''
40
+ result = self.cleaner.remove_docstrings(code)
41
+ ast.parse(result)
42
+ assert (
43
+ '"""This is a complex function with multiple parameters."""' not in result
44
+ )
45
+ assert "return True" in result
46
+
47
+ def test_multiline_function_no_docstring(self) -> None:
48
+ code = """def complex_function(
49
+ param1: str,
50
+ param2: int,
51
+ param3: dict[str, Any],
52
+ ) -> bool:
53
+ return True
54
+ """
55
+ result = self.cleaner.remove_docstrings(code)
56
+ ast.parse(result)
57
+ assert "return True" in result
58
+ assert "def complex_function(" in result
59
+
60
+ def test_nested_multiline_functions(self) -> None:
61
+ code = """class TestClass:
62
+ def outer_method(
63
+ self,
64
+ param: str,
65
+ ) -> None:
66
+ def inner_function(
67
+ x: int,
68
+ y: int,
69
+ ) -> int:
70
+ return x + y
71
+ return inner_function(1, 2)
72
+ """
73
+ result = self.cleaner.remove_docstrings(code)
74
+ ast.parse(result)
75
+ assert '"""Outer method docstring."""' not in result
76
+ assert '"""Inner function docstring."""' not in result
77
+ assert "return x + y" in result
78
+ assert "return inner_function(1, 2)" in result
79
+
80
+ def test_multiline_function_with_decorators(self) -> None:
81
+ code = """class TestClass:
82
+ @property
83
+ @some_decorator(
84
+ param1="value1",
85
+ param2="value2"
86
+ )
87
+ def complex_property(
88
+ self,
89
+ ) -> str:
90
+ return "test"
91
+ """
92
+ result = self.cleaner.remove_docstrings(code)
93
+ ast.parse(result)
94
+ assert (
95
+ '"""Property with complex decorator and multiline signature."""'
96
+ not in result
97
+ )
98
+ assert "@property" in result
99
+ assert "@some_decorator" in result
100
+ assert 'return "test"' in result
101
+
102
+ def test_class_with_multiline_init(self) -> None:
103
+ code = '''class ComplexClass:
104
+ """Class docstring."""
105
+ def __init__(
106
+ self,
107
+ param1: str,
108
+ param2: int = 42,
109
+ param3: Optional[dict] = None,
110
+ ) -> None:
111
+ self.param1 = param1
112
+ self.param2 = param2
113
+ self.param3 = param3 or {}
114
+ '''
115
+ result = self.cleaner.remove_docstrings(code)
116
+ ast.parse(result)
117
+ assert '"""Class docstring."""' not in result
118
+ assert '"""Initialize the complex class."""' not in result
119
+ assert "self.param1 = param1" in result
120
+ assert "self.param2 = param2" in result
121
+ assert "self.param3 = param3 or {}" in result
122
+
123
+ def test_multiline_function_empty_body_gets_pass(self) -> None:
124
+ code = '''def empty_function(
125
+ param1: str,
126
+ param2: int,
127
+ ) -> None:
128
+ """This function only has a docstring."""
129
+ '''
130
+ result = self.cleaner.remove_docstrings(code)
131
+ ast.parse(result)
132
+ assert '"""This function only has a docstring."""' not in result
133
+ assert "pass" in result
134
+
135
+ def test_complex_real_world_example(self) -> None:
136
+ code = '''class BackgroundTaskManager:
137
+ """Manage background tasks."""
138
+ def __init__(self, cleanup_timeout: float = 30.0) -> None:
139
+ self.cleanup_timeout = cleanup_timeout
140
+ self.logger = depends.get(Logger)
141
+ async def __aenter__(self) -> "BackgroundTaskManager":
142
+ self.logger.debug("Background task manager started")
143
+ return self
144
+ async def __aexit__(
145
+ self,
146
+ exc_type: type[BaseException] | None,
147
+ exc_val: BaseException | None,
148
+ exc_tb: t.Any | None,
149
+ ) -> None:
150
+ del exc_type, exc_val, exc_tb
151
+ self.logger.debug("Background task manager shutting down")
152
+ await wait_for_background_tasks(self.cleanup_timeout)
153
+ '''
154
+ result = self.cleaner.remove_docstrings(code)
155
+ ast.parse(result)
156
+ assert '"""Manage background tasks."""' not in result
157
+ assert '"""Initialize the background task manager."""' not in result
158
+ assert '"""Enter async context."""' not in result
159
+ assert '"""Clean up background tasks on exit."""' not in result
160
+ assert "self.cleanup_timeout = cleanup_timeout" in result
161
+ assert 'self.logger.debug("Background task manager started")' in result
162
+ assert "del exc_type, exc_val, exc_tb" in result
163
+ assert "await wait_for_background_tasks" in result
@@ -2,7 +2,6 @@ import subprocess
2
2
  from pathlib import Path
3
3
  from typing import Any
4
4
  from unittest import mock
5
-
6
5
  from crackerjack.py313 import (
7
6
  CommandResult,
8
7
  CommandRunner,
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
-
3
2
  from crackerjack.py313 import (
4
3
  CommandResult,
5
4
  HookResult,
@@ -1,5 +1,4 @@
1
1
  import time
2
-
3
2
  import pytest
4
3
 
5
4
 
@@ -2,7 +2,6 @@ import io
2
2
  import typing as t
3
3
  from pathlib import Path
4
4
  from unittest.mock import MagicMock, patch
5
-
6
5
  import pytest
7
6
  from rich.console import Console
8
7
 
File without changes