crackerjack 0.21.6__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.6 → crackerjack-0.21.7}/PKG-INFO +8 -8
  2. {crackerjack-0.21.6 → crackerjack-0.21.7}/README.md +7 -7
  3. crackerjack-0.21.7/crackerjack/.ruff_cache/0.12.1/5056746222905752453 +0 -0
  4. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/__init__.py +0 -1
  5. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/__main__.py +0 -1
  6. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/crackerjack.py +81 -70
  7. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/errors.py +0 -1
  8. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/interactive.py +0 -10
  9. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/pyproject.toml +1 -1
  10. {crackerjack-0.21.6 → crackerjack-0.21.7}/pyproject.toml +1 -1
  11. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/conftest.py +0 -1
  12. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_crackerjack.py +0 -33
  13. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_crackerjack_runner.py +0 -1
  14. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_errors.py +0 -1
  15. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_interactive.py +0 -1
  16. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_interactive_run.py +0 -1
  17. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_main.py +0 -3
  18. crackerjack-0.21.7/tests/test_multiline_functions.py +163 -0
  19. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_py313_advanced.py +0 -1
  20. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_py313_features.py +0 -1
  21. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_pytest_features.py +0 -1
  22. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/test_structured_errors.py +0 -1
  23. crackerjack-0.21.6/crackerjack/.ruff_cache/0.12.1/5056746222905752453 +0 -0
  24. {crackerjack-0.21.6 → crackerjack-0.21.7}/LICENSE +0 -0
  25. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.gitignore +0 -0
  26. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.libcst.codemod.yaml +0 -0
  27. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pdm.toml +0 -0
  28. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pre-commit-config-ai.yaml +0 -0
  29. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pre-commit-config.yaml +0 -0
  30. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pytest_cache/.gitignore +0 -0
  31. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pytest_cache/CACHEDIR.TAG +0 -0
  32. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pytest_cache/README.md +0 -0
  33. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pytest_cache/v/cache/nodeids +0 -0
  34. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.pytest_cache/v/cache/stepwise +0 -0
  35. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/.gitignore +0 -0
  36. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
  37. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
  38. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
  39. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
  40. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
  41. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
  42. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.11/18187162184424859798 +0 -0
  43. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/16869036553936192448 +0 -0
  44. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
  45. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
  46. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.12/4441409093023629623 +0 -0
  47. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
  48. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.13/4240757255861806333 +0 -0
  49. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
  50. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
  51. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
  52. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
  53. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
  54. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
  55. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
  56. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.12.0/5056746222905752453 +0 -0
  57. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
  58. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
  59. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
  60. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
  61. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
  62. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
  63. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
  64. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
  65. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
  66. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
  67. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
  68. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
  69. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
  70. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
  71. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
  72. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
  73. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
  74. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
  75. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
  76. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
  77. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
  78. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
  79. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
  80. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
  81. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
  82. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
  83. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/.ruff_cache/CACHEDIR.TAG +0 -0
  84. {crackerjack-0.21.6 → crackerjack-0.21.7}/crackerjack/py313.py +0 -0
  85. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/TESTING.md +0 -0
  86. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/__init__.py +0 -0
  87. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/data/comments_sample.txt +0 -0
  88. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/data/docstrings_sample.txt +0 -0
  89. {crackerjack-0.21.6 → crackerjack-0.21.7}/tests/data/expected_comments_sample.txt +0 -0
  90. {crackerjack-0.21.6 → 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.6
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
@@ -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,55 +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
 
89
- def remove_docstrings(self, code: str) -> str:
90
- lines = code.split("\n")
91
- cleaned_lines = []
92
- docstring_state = {
88
+ def _initialize_docstring_state(self) -> dict[str, t.Any]:
89
+ return {
93
90
  "in_docstring": False,
94
91
  "delimiter": None,
95
92
  "waiting": False,
96
93
  "function_indent": 0,
97
94
  "removed_docstring": False,
95
+ "in_multiline_def": False,
98
96
  }
99
- for i, line in enumerate(lines):
100
- stripped = line.strip()
101
- if self._is_function_or_class_definition(stripped):
102
- docstring_state["waiting"] = True
103
- docstring_state["function_indent"] = len(line) - len(line.lstrip())
104
- docstring_state["removed_docstring"] = False
105
- cleaned_lines.append(line)
106
- continue
107
- if docstring_state["waiting"] and stripped:
108
- if self._handle_docstring_start(stripped, docstring_state):
109
- if not docstring_state["in_docstring"]:
110
- if self._needs_pass_statement(
111
- lines, i + 1, docstring_state["function_indent"]
112
- ):
113
- pass_line = (
114
- " " * (docstring_state["function_indent"] + 4) + "pass"
115
- )
116
- cleaned_lines.append(pass_line)
117
- docstring_state["removed_docstring"] = True
118
- continue
119
- else:
120
- docstring_state["waiting"] = False
121
- if docstring_state["in_docstring"]:
122
- if self._handle_docstring_end(stripped, docstring_state):
123
- if self._needs_pass_statement(
124
- lines, i + 1, docstring_state["function_indent"]
125
- ):
126
- pass_line = (
127
- " " * (docstring_state["function_indent"] + 4) + "pass"
128
- )
129
- cleaned_lines.append(pass_line)
130
- docstring_state["removed_docstring"] = False
131
- continue
132
- else:
133
- continue
134
- if docstring_state["removed_docstring"] and stripped:
135
- docstring_state["removed_docstring"] = False
136
- cleaned_lines.append(line)
137
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
+
167
+ def remove_docstrings(self, code: str) -> str:
168
+ lines = code.split("\n")
169
+ cleaned_lines = []
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)
138
175
  return "\n".join(cleaned_lines)
139
176
 
140
177
  def _is_function_or_class_definition(self, stripped_line: str) -> bool:
@@ -177,9 +214,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
177
214
  line_indent = len(line) - len(line.lstrip())
178
215
  if line_indent <= function_indent:
179
216
  return True
180
- if line_indent == function_indent + 4:
217
+ if line_indent > function_indent:
181
218
  return False
182
-
183
219
  return True
184
220
 
185
221
  def remove_line_comments(self, code: str) -> str:
@@ -192,7 +228,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
192
228
  cleaned_line = self._process_line_for_comments(line)
193
229
  if cleaned_line or not line.strip():
194
230
  cleaned_lines.append(cleaned_line or line)
195
-
196
231
  return "\n".join(cleaned_lines)
197
232
 
198
233
  def _process_line_for_comments(self, line: str) -> str:
@@ -205,7 +240,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
205
240
  break
206
241
  else:
207
242
  result.append(char)
208
-
209
243
  return "".join(result).rstrip()
210
244
 
211
245
  def _handle_string_character(
@@ -216,18 +250,14 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
216
250
  string_state: dict[str, t.Any],
217
251
  result: list[str],
218
252
  ) -> bool:
219
- """Handle string quote characters. Returns True if character was handled."""
220
253
  if char not in ("'", '"'):
221
254
  return False
222
-
223
255
  if index > 0 and line[index - 1] == "\\":
224
256
  return False
225
-
226
257
  if string_state["in_string"] is None:
227
258
  string_state["in_string"] = char
228
259
  elif string_state["in_string"] == char:
229
260
  string_state["in_string"] = None
230
-
231
261
  result.append(char)
232
262
  return True
233
263
 
@@ -239,14 +269,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
239
269
  string_state: dict[str, t.Any],
240
270
  result: list[str],
241
271
  ) -> bool:
242
- """Handle comment character. Returns True if comment was found."""
243
272
  if char != "#" or string_state["in_string"] is not None:
244
273
  return False
245
-
246
274
  comment = line[index:].strip()
247
275
  if self._is_special_comment_line(comment):
248
276
  result.append(line[index:])
249
-
250
277
  return True
251
278
 
252
279
  def _is_special_comment_line(self, comment: str) -> bool:
@@ -270,13 +297,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
270
297
  ):
271
298
  continue
272
299
  cleaned_lines.append(line)
273
-
274
300
  return "\n".join(self._remove_trailing_empty_lines(cleaned_lines))
275
301
 
276
302
  def _update_function_state(
277
303
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
278
304
  ) -> None:
279
- """Update function tracking state based on current line."""
280
305
  if stripped_line.startswith(("def ", "async def ")):
281
306
  function_tracker["in_function"] = True
282
307
  function_tracker["function_indent"] = len(line) - len(stripped_line)
@@ -287,7 +312,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
287
312
  def _is_function_end(
288
313
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
289
314
  ) -> bool:
290
- """Check if current line marks the end of a function."""
291
315
  return (
292
316
  function_tracker["in_function"]
293
317
  and bool(line)
@@ -302,13 +326,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
302
326
  cleaned_lines: list[str],
303
327
  function_tracker: dict[str, t.Any],
304
328
  ) -> bool:
305
- """Determine if an empty line should be skipped."""
306
329
  if line_idx > 0 and cleaned_lines and (not cleaned_lines[-1]):
307
330
  return True
308
-
309
331
  if function_tracker["in_function"]:
310
332
  return self._should_skip_function_empty_line(line_idx, lines)
311
-
312
333
  return False
313
334
 
314
335
  def _should_skip_function_empty_line(self, line_idx: int, lines: list[str]) -> bool:
@@ -323,7 +344,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
323
344
  return True
324
345
  if next_line in ("pass", "break", "continue", "raise"):
325
346
  return True
326
-
327
347
  return self._is_special_comment(next_line)
328
348
 
329
349
  def _is_special_comment(self, line: str) -> bool:
@@ -463,15 +483,12 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
463
483
  for tool, settings in our_toml_config.get("tool", {}).items():
464
484
  if tool not in pkg_toml_config["tool"]:
465
485
  pkg_toml_config["tool"][tool] = {}
466
-
467
486
  pkg_tool_config = pkg_toml_config["tool"][tool]
468
-
469
487
  self._merge_tool_config(settings, pkg_tool_config, tool)
470
488
 
471
489
  def _merge_tool_config(
472
490
  self, our_config: dict[str, t.Any], pkg_config: dict[str, t.Any], tool: str
473
491
  ) -> None:
474
- """Recursively merge tool configuration, preserving existing project settings."""
475
492
  for setting, value in our_config.items():
476
493
  if isinstance(value, dict):
477
494
  self._merge_nested_config(setting, value, pkg_config)
@@ -481,21 +498,17 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
481
498
  def _merge_nested_config(
482
499
  self, setting: str, value: dict[str, t.Any], pkg_config: dict[str, t.Any]
483
500
  ) -> None:
484
- """Handle nested configuration merging."""
485
501
  if setting not in pkg_config:
486
502
  pkg_config[setting] = {}
487
503
  elif not isinstance(pkg_config[setting], dict):
488
504
  pkg_config[setting] = {}
489
-
490
505
  self._merge_tool_config(value, pkg_config[setting], "")
491
-
492
506
  for k, v in value.items():
493
507
  self._merge_nested_value(k, v, pkg_config[setting])
494
508
 
495
509
  def _merge_nested_value(
496
510
  self, key: str, value: t.Any, nested_config: dict[str, t.Any]
497
511
  ) -> None:
498
- """Merge individual nested values."""
499
512
  if isinstance(value, str | list) and "crackerjack" in str(value):
500
513
  nested_config[key] = self.swap_package_name(value)
501
514
  elif self._is_mergeable_list(key, value):
@@ -510,7 +523,6 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
510
523
  def _merge_direct_config(
511
524
  self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
512
525
  ) -> None:
513
- """Handle direct configuration merging."""
514
526
  if isinstance(value, str | list) and "crackerjack" in str(value):
515
527
  pkg_config[setting] = self.swap_package_name(value)
516
528
  elif self._is_mergeable_list(setting, value):
@@ -773,7 +785,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
773
785
  self._add_benchmark_flags(test, options)
774
786
  else:
775
787
  self._add_worker_flags(test, options, project_size)
776
-
777
788
  return test
778
789
 
779
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.5"
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.6"
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
  )
@@ -1125,29 +1100,21 @@ class TestCrackerjackProcess:
1125
1100
  test_code = """
1126
1101
  def empty_function():
1127
1102
  pass
1128
-
1129
1103
  class TestClass:
1130
-
1131
1104
  def method_with_docstring_only(self):
1132
1105
  pass
1133
-
1134
1106
  def method_with_code(self):
1135
1107
  return True
1136
1108
  """
1137
-
1138
1109
  cleaned_code = code_cleaner.remove_docstrings(test_code)
1139
1110
  print(f"Cleaned code: {cleaned_code!r}")
1140
-
1141
1111
  assert '"""This function has only a docstring."""' not in cleaned_code
1142
1112
  assert '"""Class docstring."""' not in cleaned_code
1143
1113
  assert '"""Method with only docstring."""' not in cleaned_code
1144
1114
  assert '"""This method has code after docstring."""' not in cleaned_code
1145
-
1146
1115
  assert "def empty_function():\n pass" in cleaned_code
1147
1116
  assert "def method_with_docstring_only(self):\n pass" in cleaned_code
1148
-
1149
1117
  assert "def method_with_code(self):\n return True" in cleaned_code
1150
-
1151
1118
  try:
1152
1119
  ast.parse(cleaned_code)
1153
1120
  except SyntaxError as e:
@@ -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