crackerjack 0.20.20__tar.gz → 0.21.0__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.
- {crackerjack-0.20.20 → crackerjack-0.21.0}/PKG-INFO +4 -4
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pre-commit-config.yaml +10 -4
- crackerjack-0.21.0/crackerjack/.ruff_cache/0.12.0/5056746222905752453 +0 -0
- crackerjack-0.21.0/crackerjack/.ruff_cache/0.12.1/5056746222905752453 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/__main__.py +5 -8
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/crackerjack.py +241 -121
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/interactive.py +119 -66
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/pyproject.toml +7 -4
- {crackerjack-0.20.20 → crackerjack-0.21.0}/pyproject.toml +7 -4
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_crackerjack.py +62 -104
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_main.py +19 -12
- {crackerjack-0.20.20 → crackerjack-0.21.0}/LICENSE +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/README.md +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.gitignore +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.libcst.codemod.yaml +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pdm.toml +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pytest_cache/.gitignore +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pytest_cache/CACHEDIR.TAG +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pytest_cache/README.md +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pytest_cache/v/cache/nodeids +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.pytest_cache/v/cache/stepwise +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/.gitignore +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.11/18187162184424859798 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.12/16869036553936192448 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.12/4441409093023629623 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.13/4240757255861806333 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/.ruff_cache/CACHEDIR.TAG +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/__init__.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/errors.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/crackerjack/py313.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/TESTING.md +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/__init__.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/conftest.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/data/comments_sample.txt +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/data/docstrings_sample.txt +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/data/expected_comments_sample.txt +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/data/init.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_crackerjack_runner.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_errors.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_interactive.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_interactive_run.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_py313_advanced.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_py313_features.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_pytest_features.py +0 -0
- {crackerjack-0.20.20 → crackerjack-0.21.0}/tests/test_structured_errors.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: crackerjack
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.21.0
|
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>
|
@@ -24,11 +24,11 @@ Project-URL: repository, https://github.com/lesleslie/crackerjack
|
|
24
24
|
Requires-Python: >=3.13
|
25
25
|
Requires-Dist: autotyping>=24.9
|
26
26
|
Requires-Dist: keyring>=25.6
|
27
|
-
Requires-Dist: pdm>=2.25.
|
27
|
+
Requires-Dist: pdm>=2.25.3
|
28
28
|
Requires-Dist: pdm-bump>=0.9.12
|
29
29
|
Requires-Dist: pre-commit>=4.2
|
30
30
|
Requires-Dist: pydantic>=2.11.7
|
31
|
-
Requires-Dist: pytest>=8.4
|
31
|
+
Requires-Dist: pytest>=8.4.1
|
32
32
|
Requires-Dist: pytest-asyncio>=1
|
33
33
|
Requires-Dist: pytest-benchmark>=5.1
|
34
34
|
Requires-Dist: pytest-cov>=6.2.1
|
@@ -39,7 +39,7 @@ Requires-Dist: pyyaml>=6.0.2
|
|
39
39
|
Requires-Dist: rich>=14
|
40
40
|
Requires-Dist: tomli-w>=1.2
|
41
41
|
Requires-Dist: typer>=0.16
|
42
|
-
Requires-Dist: uv>=0.7.
|
42
|
+
Requires-Dist: uv>=0.7.15
|
43
43
|
Description-Content-Type: text/markdown
|
44
44
|
|
45
45
|
# Crackerjack: Elevate Your Python Development
|
@@ -26,7 +26,7 @@ repos:
|
|
26
26
|
|
27
27
|
# Package management - once structure is valid
|
28
28
|
- repo: https://github.com/pdm-project/pdm
|
29
|
-
rev: 2.
|
29
|
+
rev: 2.25.3
|
30
30
|
hooks:
|
31
31
|
- id: pdm-lock-check
|
32
32
|
- id: pdm-sync
|
@@ -34,7 +34,7 @@ repos:
|
|
34
34
|
- keyring
|
35
35
|
|
36
36
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
37
|
-
rev: 0.7.
|
37
|
+
rev: 0.7.15
|
38
38
|
hooks:
|
39
39
|
- id: uv-lock
|
40
40
|
files: ^pyproject\.toml$
|
@@ -55,7 +55,7 @@ repos:
|
|
55
55
|
- tomli
|
56
56
|
|
57
57
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
58
|
-
rev: v0.
|
58
|
+
rev: v0.12.1
|
59
59
|
hooks:
|
60
60
|
- id: ruff-check
|
61
61
|
- id: ruff-format
|
@@ -71,6 +71,12 @@ repos:
|
|
71
71
|
hooks:
|
72
72
|
- id: creosote
|
73
73
|
|
74
|
+
- repo: https://github.com/rohaquinlop/complexipy-pre-commit
|
75
|
+
rev: v3.0.0
|
76
|
+
hooks:
|
77
|
+
- id: complexipy
|
78
|
+
args: ["-d", "low"]
|
79
|
+
|
74
80
|
- repo: https://github.com/dosisod/refurb
|
75
81
|
rev: v2.1.0
|
76
82
|
hooks:
|
@@ -95,7 +101,7 @@ repos:
|
|
95
101
|
- libcst>=1.1.0
|
96
102
|
|
97
103
|
- repo: https://github.com/PyCQA/bandit
|
98
|
-
rev: '1.8.
|
104
|
+
rev: '1.8.5'
|
99
105
|
hooks:
|
100
106
|
- id: bandit
|
101
107
|
args: ["-c", "pyproject.toml"]
|
Binary file
|
Binary file
|
@@ -40,7 +40,6 @@ class Options(BaseModel):
|
|
40
40
|
ai_agent: bool = False
|
41
41
|
create_pr: bool = False
|
42
42
|
skip_hooks: bool = False
|
43
|
-
rich_ui: bool = False
|
44
43
|
|
45
44
|
@classmethod
|
46
45
|
@field_validator("publish", "bump", mode="before")
|
@@ -59,7 +58,10 @@ class Options(BaseModel):
|
|
59
58
|
cli_options = {
|
60
59
|
"commit": typer.Option(False, "-c", "--commit", help="Commit changes to Git."),
|
61
60
|
"interactive": typer.Option(
|
62
|
-
False,
|
61
|
+
False,
|
62
|
+
"-i",
|
63
|
+
"--interactive",
|
64
|
+
help="Use the interactive Rich UI for a better experience.",
|
63
65
|
),
|
64
66
|
"doc": typer.Option(False, "-d", "--doc", help="Generate documentation."),
|
65
67
|
"no_config_updates": typer.Option(
|
@@ -131,9 +133,6 @@ cli_options = {
|
|
131
133
|
"create_pr": typer.Option(
|
132
134
|
False, "-r", "--pr", help="Create a pull request to the upstream repository."
|
133
135
|
),
|
134
|
-
"rich_ui": typer.Option(
|
135
|
-
False, "--rich-ui", help="Use the interactive Rich UI for a better experience."
|
136
|
-
),
|
137
136
|
"ai_agent": typer.Option(
|
138
137
|
False,
|
139
138
|
"--ai-agent",
|
@@ -165,7 +164,6 @@ def main(
|
|
165
164
|
test_timeout: int = cli_options["test_timeout"],
|
166
165
|
skip_hooks: bool = cli_options["skip_hooks"],
|
167
166
|
create_pr: bool = cli_options["create_pr"],
|
168
|
-
rich_ui: bool = cli_options["rich_ui"],
|
169
167
|
ai_agent: bool = cli_options["ai_agent"],
|
170
168
|
) -> None:
|
171
169
|
options = Options(
|
@@ -188,13 +186,12 @@ def main(
|
|
188
186
|
all=all,
|
189
187
|
ai_agent=ai_agent,
|
190
188
|
create_pr=create_pr,
|
191
|
-
rich_ui=rich_ui,
|
192
189
|
)
|
193
190
|
if ai_agent:
|
194
191
|
import os
|
195
192
|
|
196
193
|
os.environ["AI_AGENT"] = "1"
|
197
|
-
if
|
194
|
+
if interactive:
|
198
195
|
from crackerjack.interactive import launch_interactive_cli
|
199
196
|
|
200
197
|
try:
|
@@ -13,7 +13,6 @@ from tomli_w import dumps
|
|
13
13
|
from crackerjack.errors import ErrorCode, ExecutionError
|
14
14
|
|
15
15
|
config_files = (".gitignore", ".pre-commit-config.yaml", ".libcst.codemod.yaml")
|
16
|
-
interactive_hooks = ("refurb", "bandit", "pyright")
|
17
16
|
default_python_version = "3.13"
|
18
17
|
|
19
18
|
|
@@ -85,36 +84,20 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
85
84
|
def remove_docstrings(self, code: str) -> str:
|
86
85
|
lines = code.split("\n")
|
87
86
|
cleaned_lines = []
|
88
|
-
|
89
|
-
docstring_delimiter = None
|
90
|
-
waiting_for_docstring = False
|
87
|
+
docstring_state = {"in_docstring": False, "delimiter": None, "waiting": False}
|
91
88
|
for line in lines:
|
92
89
|
stripped = line.strip()
|
93
|
-
if
|
94
|
-
|
90
|
+
if self._is_function_or_class_definition(stripped):
|
91
|
+
docstring_state["waiting"] = True
|
95
92
|
cleaned_lines.append(line)
|
96
93
|
continue
|
97
|
-
if
|
98
|
-
if
|
99
|
-
|
100
|
-
docstring_delimiter = stripped[:3]
|
101
|
-
else:
|
102
|
-
docstring_delimiter = stripped[0]
|
103
|
-
if stripped.endswith(docstring_delimiter) and len(stripped) > len(
|
104
|
-
docstring_delimiter
|
105
|
-
):
|
106
|
-
waiting_for_docstring = False
|
107
|
-
continue
|
108
|
-
else:
|
109
|
-
in_docstring = True
|
110
|
-
waiting_for_docstring = False
|
111
|
-
continue
|
94
|
+
if docstring_state["waiting"] and stripped:
|
95
|
+
if self._handle_docstring_start(stripped, docstring_state):
|
96
|
+
continue
|
112
97
|
else:
|
113
|
-
|
114
|
-
if in_docstring:
|
115
|
-
if
|
116
|
-
in_docstring = False
|
117
|
-
docstring_delimiter = None
|
98
|
+
docstring_state["waiting"] = False
|
99
|
+
if docstring_state["in_docstring"]:
|
100
|
+
if self._handle_docstring_end(stripped, docstring_state):
|
118
101
|
continue
|
119
102
|
else:
|
120
103
|
continue
|
@@ -122,6 +105,35 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
122
105
|
|
123
106
|
return "\n".join(cleaned_lines)
|
124
107
|
|
108
|
+
def _is_function_or_class_definition(self, stripped_line: str) -> bool:
|
109
|
+
return stripped_line.startswith(("def ", "class ", "async def "))
|
110
|
+
|
111
|
+
def _handle_docstring_start(self, stripped: str, state: dict[str, t.Any]) -> bool:
|
112
|
+
if not stripped.startswith(('"""', "'''", '"', "'")):
|
113
|
+
return False
|
114
|
+
if stripped.startswith(('"""', "'''")):
|
115
|
+
delimiter = stripped[:3]
|
116
|
+
else:
|
117
|
+
delimiter = stripped[0]
|
118
|
+
state["delimiter"] = delimiter
|
119
|
+
if self._is_single_line_docstring(stripped, delimiter):
|
120
|
+
state["waiting"] = False
|
121
|
+
return True
|
122
|
+
else:
|
123
|
+
state["in_docstring"] = True
|
124
|
+
state["waiting"] = False
|
125
|
+
return True
|
126
|
+
|
127
|
+
def _is_single_line_docstring(self, stripped: str, delimiter: str) -> bool:
|
128
|
+
return stripped.endswith(delimiter) and len(stripped) > len(delimiter)
|
129
|
+
|
130
|
+
def _handle_docstring_end(self, stripped: str, state: dict[str, t.Any]) -> bool:
|
131
|
+
if state["delimiter"] and stripped.endswith(state["delimiter"]):
|
132
|
+
state["in_docstring"] = False
|
133
|
+
state["delimiter"] = None
|
134
|
+
return True
|
135
|
+
return True
|
136
|
+
|
125
137
|
def remove_line_comments(self, code: str) -> str:
|
126
138
|
lines = code.split("\n")
|
127
139
|
cleaned_lines = []
|
@@ -129,87 +141,153 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
129
141
|
if not line.strip():
|
130
142
|
cleaned_lines.append(line)
|
131
143
|
continue
|
132
|
-
|
133
|
-
result = []
|
134
|
-
i = 0
|
135
|
-
n = len(line)
|
136
|
-
while i < n:
|
137
|
-
char = line[i]
|
138
|
-
if char in ("'", '"') and (i == 0 or line[i - 1] != "\\"):
|
139
|
-
if in_string is None:
|
140
|
-
in_string = char
|
141
|
-
elif in_string == char:
|
142
|
-
in_string = None
|
143
|
-
result.append(char)
|
144
|
-
i += 1
|
145
|
-
elif char == "#" and in_string is None:
|
146
|
-
comment = line[i:].strip()
|
147
|
-
if re.match(
|
148
|
-
r"^#\s*(?:type:\s*ignore(?:\[.*?\])?|noqa|nosec|pragma:\s*no\s*cover|pylint:\s*disable|mypy:\s*ignore)",
|
149
|
-
comment,
|
150
|
-
):
|
151
|
-
result.append(line[i:])
|
152
|
-
break
|
153
|
-
break
|
154
|
-
else:
|
155
|
-
result.append(char)
|
156
|
-
i += 1
|
157
|
-
cleaned_line = "".join(result).rstrip()
|
144
|
+
cleaned_line = self._process_line_for_comments(line)
|
158
145
|
if cleaned_line or not line.strip():
|
159
146
|
cleaned_lines.append(cleaned_line or line)
|
147
|
+
|
160
148
|
return "\n".join(cleaned_lines)
|
161
149
|
|
150
|
+
def _process_line_for_comments(self, line: str) -> str:
|
151
|
+
result = []
|
152
|
+
string_state = {"in_string": None}
|
153
|
+
for i, char in enumerate(line):
|
154
|
+
if self._handle_string_character(char, i, line, string_state, result):
|
155
|
+
continue
|
156
|
+
elif self._handle_comment_character(char, i, line, string_state, result):
|
157
|
+
break
|
158
|
+
else:
|
159
|
+
result.append(char)
|
160
|
+
|
161
|
+
return "".join(result).rstrip()
|
162
|
+
|
163
|
+
def _handle_string_character(
|
164
|
+
self,
|
165
|
+
char: str,
|
166
|
+
index: int,
|
167
|
+
line: str,
|
168
|
+
string_state: dict[str, t.Any],
|
169
|
+
result: list[str],
|
170
|
+
) -> bool:
|
171
|
+
"""Handle string quote characters. Returns True if character was handled."""
|
172
|
+
if char not in ("'", '"'):
|
173
|
+
return False
|
174
|
+
|
175
|
+
if index > 0 and line[index - 1] == "\\":
|
176
|
+
return False
|
177
|
+
|
178
|
+
if string_state["in_string"] is None:
|
179
|
+
string_state["in_string"] = char
|
180
|
+
elif string_state["in_string"] == char:
|
181
|
+
string_state["in_string"] = None
|
182
|
+
|
183
|
+
result.append(char)
|
184
|
+
return True
|
185
|
+
|
186
|
+
def _handle_comment_character(
|
187
|
+
self,
|
188
|
+
char: str,
|
189
|
+
index: int,
|
190
|
+
line: str,
|
191
|
+
string_state: dict[str, t.Any],
|
192
|
+
result: list[str],
|
193
|
+
) -> bool:
|
194
|
+
"""Handle comment character. Returns True if comment was found."""
|
195
|
+
if char != "#" or string_state["in_string"] is not None:
|
196
|
+
return False
|
197
|
+
|
198
|
+
comment = line[index:].strip()
|
199
|
+
if self._is_special_comment_line(comment):
|
200
|
+
result.append(line[index:])
|
201
|
+
|
202
|
+
return True
|
203
|
+
|
204
|
+
def _is_special_comment_line(self, comment: str) -> bool:
|
205
|
+
special_comment_pattern = (
|
206
|
+
r"^#\s*(?:type:\s*ignore(?:\[.*?\])?|noqa|nosec|pragma:\s*no\s*cover"
|
207
|
+
r"|pylint:\s*disable|mypy:\s*ignore)"
|
208
|
+
)
|
209
|
+
return bool(re.match(special_comment_pattern, comment))
|
210
|
+
|
162
211
|
def remove_extra_whitespace(self, code: str) -> str:
|
163
212
|
lines = code.split("\n")
|
164
213
|
cleaned_lines = []
|
165
|
-
|
166
|
-
function_indent = 0
|
214
|
+
function_tracker = {"in_function": False, "function_indent": 0}
|
167
215
|
for i, line in enumerate(lines):
|
168
216
|
line = line.rstrip()
|
169
217
|
stripped_line = line.lstrip()
|
170
|
-
|
171
|
-
in_function = True
|
172
|
-
function_indent = len(line) - len(stripped_line)
|
173
|
-
elif (
|
174
|
-
in_function
|
175
|
-
and line
|
176
|
-
and (len(line) - len(stripped_line) <= function_indent)
|
177
|
-
and (not stripped_line.startswith(("@", "#")))
|
178
|
-
):
|
179
|
-
in_function = False
|
180
|
-
function_indent = 0
|
218
|
+
self._update_function_state(line, stripped_line, function_tracker)
|
181
219
|
if not line:
|
182
|
-
if
|
220
|
+
if self._should_skip_empty_line(
|
221
|
+
i, lines, cleaned_lines, function_tracker
|
222
|
+
):
|
183
223
|
continue
|
184
|
-
if in_function:
|
185
|
-
next_line_idx = i + 1
|
186
|
-
if next_line_idx < len(lines):
|
187
|
-
next_line = lines[next_line_idx].strip()
|
188
|
-
if not (
|
189
|
-
next_line.startswith(
|
190
|
-
("return", "class ", "def ", "async def ", "@")
|
191
|
-
)
|
192
|
-
or next_line in ("pass", "break", "continue", "raise")
|
193
|
-
or (
|
194
|
-
next_line.startswith("#")
|
195
|
-
and any(
|
196
|
-
pattern in next_line
|
197
|
-
for pattern in (
|
198
|
-
"type:",
|
199
|
-
"noqa",
|
200
|
-
"nosec",
|
201
|
-
"pragma:",
|
202
|
-
"pylint:",
|
203
|
-
"mypy:",
|
204
|
-
)
|
205
|
-
)
|
206
|
-
)
|
207
|
-
):
|
208
|
-
continue
|
209
224
|
cleaned_lines.append(line)
|
210
|
-
|
211
|
-
|
212
|
-
|
225
|
+
|
226
|
+
return "\n".join(self._remove_trailing_empty_lines(cleaned_lines))
|
227
|
+
|
228
|
+
def _update_function_state(
|
229
|
+
self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
|
230
|
+
) -> None:
|
231
|
+
"""Update function tracking state based on current line."""
|
232
|
+
if stripped_line.startswith(("def ", "async def ")):
|
233
|
+
function_tracker["in_function"] = True
|
234
|
+
function_tracker["function_indent"] = len(line) - len(stripped_line)
|
235
|
+
elif self._is_function_end(line, stripped_line, function_tracker):
|
236
|
+
function_tracker["in_function"] = False
|
237
|
+
function_tracker["function_indent"] = 0
|
238
|
+
|
239
|
+
def _is_function_end(
|
240
|
+
self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
|
241
|
+
) -> bool:
|
242
|
+
"""Check if current line marks the end of a function."""
|
243
|
+
return (
|
244
|
+
function_tracker["in_function"]
|
245
|
+
and bool(line)
|
246
|
+
and (len(line) - len(stripped_line) <= function_tracker["function_indent"])
|
247
|
+
and (not stripped_line.startswith(("@", "#")))
|
248
|
+
)
|
249
|
+
|
250
|
+
def _should_skip_empty_line(
|
251
|
+
self,
|
252
|
+
line_idx: int,
|
253
|
+
lines: list[str],
|
254
|
+
cleaned_lines: list[str],
|
255
|
+
function_tracker: dict[str, t.Any],
|
256
|
+
) -> bool:
|
257
|
+
"""Determine if an empty line should be skipped."""
|
258
|
+
if line_idx > 0 and cleaned_lines and (not cleaned_lines[-1]):
|
259
|
+
return True
|
260
|
+
|
261
|
+
if function_tracker["in_function"]:
|
262
|
+
return self._should_skip_function_empty_line(line_idx, lines)
|
263
|
+
|
264
|
+
return False
|
265
|
+
|
266
|
+
def _should_skip_function_empty_line(self, line_idx: int, lines: list[str]) -> bool:
|
267
|
+
next_line_idx = line_idx + 1
|
268
|
+
if next_line_idx >= len(lines):
|
269
|
+
return False
|
270
|
+
next_line = lines[next_line_idx].strip()
|
271
|
+
return not self._is_significant_next_line(next_line)
|
272
|
+
|
273
|
+
def _is_significant_next_line(self, next_line: str) -> bool:
|
274
|
+
if next_line.startswith(("return", "class ", "def ", "async def ", "@")):
|
275
|
+
return True
|
276
|
+
if next_line in ("pass", "break", "continue", "raise"):
|
277
|
+
return True
|
278
|
+
|
279
|
+
return self._is_special_comment(next_line)
|
280
|
+
|
281
|
+
def _is_special_comment(self, line: str) -> bool:
|
282
|
+
if not line.startswith("#"):
|
283
|
+
return False
|
284
|
+
special_patterns = ("type:", "noqa", "nosec", "pragma:", "pylint:", "mypy:")
|
285
|
+
return any(pattern in line for pattern in special_patterns)
|
286
|
+
|
287
|
+
def _remove_trailing_empty_lines(self, lines: list[str]) -> list[str]:
|
288
|
+
while lines and (not lines[-1]):
|
289
|
+
lines.pop()
|
290
|
+
return lines
|
213
291
|
|
214
292
|
def reformat_code(self, code: str) -> str:
|
215
293
|
from crackerjack.errors import handle_error
|
@@ -335,27 +413,75 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
335
413
|
self, our_toml_config: dict[str, t.Any], pkg_toml_config: dict[str, t.Any]
|
336
414
|
) -> None:
|
337
415
|
for tool, settings in our_toml_config.get("tool", {}).items():
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
416
|
+
if tool not in pkg_toml_config["tool"]:
|
417
|
+
pkg_toml_config["tool"][tool] = {}
|
418
|
+
|
419
|
+
pkg_tool_config = pkg_toml_config["tool"][tool]
|
420
|
+
|
421
|
+
self._merge_tool_config(settings, pkg_tool_config, tool)
|
422
|
+
|
423
|
+
def _merge_tool_config(
|
424
|
+
self, our_config: dict[str, t.Any], pkg_config: dict[str, t.Any], tool: str
|
425
|
+
) -> None:
|
426
|
+
"""Recursively merge tool configuration, preserving existing project settings."""
|
427
|
+
for setting, value in our_config.items():
|
428
|
+
if isinstance(value, dict):
|
429
|
+
self._merge_nested_config(setting, value, pkg_config)
|
430
|
+
else:
|
431
|
+
self._merge_direct_config(setting, value, pkg_config)
|
432
|
+
|
433
|
+
def _merge_nested_config(
|
434
|
+
self, setting: str, value: dict[str, t.Any], pkg_config: dict[str, t.Any]
|
435
|
+
) -> None:
|
436
|
+
"""Handle nested configuration merging."""
|
437
|
+
if setting not in pkg_config:
|
438
|
+
pkg_config[setting] = {}
|
439
|
+
elif not isinstance(pkg_config[setting], dict):
|
440
|
+
pkg_config[setting] = {}
|
441
|
+
|
442
|
+
self._merge_tool_config(value, pkg_config[setting], "")
|
443
|
+
|
444
|
+
for k, v in value.items():
|
445
|
+
self._merge_nested_value(k, v, pkg_config[setting])
|
446
|
+
|
447
|
+
def _merge_nested_value(
|
448
|
+
self, key: str, value: t.Any, nested_config: dict[str, t.Any]
|
449
|
+
) -> None:
|
450
|
+
"""Merge individual nested values."""
|
451
|
+
if isinstance(value, str | list) and "crackerjack" in str(value):
|
452
|
+
nested_config[key] = self.swap_package_name(value)
|
453
|
+
elif self._is_mergeable_list(key, value):
|
454
|
+
existing = nested_config.get(key, [])
|
455
|
+
if isinstance(existing, list) and isinstance(value, list):
|
456
|
+
nested_config[key] = list(set(existing + value))
|
457
|
+
else:
|
458
|
+
nested_config[key] = value
|
459
|
+
elif key not in nested_config:
|
460
|
+
nested_config[key] = value
|
461
|
+
|
462
|
+
def _merge_direct_config(
|
463
|
+
self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
|
464
|
+
) -> None:
|
465
|
+
"""Handle direct configuration merging."""
|
466
|
+
if isinstance(value, str | list) and "crackerjack" in str(value):
|
467
|
+
pkg_config[setting] = self.swap_package_name(value)
|
468
|
+
elif self._is_mergeable_list(setting, value):
|
469
|
+
existing = pkg_config.get(setting, [])
|
470
|
+
if isinstance(existing, list) and isinstance(value, list):
|
471
|
+
pkg_config[setting] = list(set(existing + value))
|
472
|
+
else:
|
473
|
+
pkg_config[setting] = value
|
474
|
+
elif setting not in pkg_config:
|
475
|
+
pkg_config[setting] = value
|
476
|
+
|
477
|
+
def _is_mergeable_list(self, key: str, value: t.Any) -> bool:
|
478
|
+
return key in (
|
479
|
+
"exclude-deps",
|
480
|
+
"exclude",
|
481
|
+
"excluded",
|
482
|
+
"skips",
|
483
|
+
"ignore",
|
484
|
+
) and isinstance(value, list)
|
359
485
|
|
360
486
|
def _update_python_version(
|
361
487
|
self, our_toml_config: dict[str, t.Any], pkg_toml_config: dict[str, t.Any]
|
@@ -520,11 +646,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
520
646
|
if self.pkg_path.stem == "crackerjack" and options.update_precommit:
|
521
647
|
self.execute_command(["pre-commit", "autoupdate"])
|
522
648
|
|
523
|
-
def _run_interactive_hooks(self, options: t.Any) -> None:
|
524
|
-
if options.interactive:
|
525
|
-
for hook in interactive_hooks:
|
526
|
-
self.project_manager.run_interactive(hook)
|
527
|
-
|
528
649
|
def _clean_project(self, options: t.Any) -> None:
|
529
650
|
if options.clean:
|
530
651
|
if self.pkg_dir:
|
@@ -655,7 +776,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
655
776
|
self._setup_package()
|
656
777
|
self._update_project(options)
|
657
778
|
self._update_precommit(options)
|
658
|
-
self._run_interactive_hooks(options)
|
659
779
|
self._clean_project(options)
|
660
780
|
if not options.skip_hooks:
|
661
781
|
self.project_manager.run_pre_commit()
|