prune-cli 0.1.0__py3-none-any.whl
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.
- prune/__init__.py +1 -0
- prune/main.py +418 -0
- prune/transformers.py +247 -0
- prune_cli-0.1.0.dist-info/METADATA +86 -0
- prune_cli-0.1.0.dist-info/RECORD +9 -0
- prune_cli-0.1.0.dist-info/WHEEL +5 -0
- prune_cli-0.1.0.dist-info/entry_points.txt +2 -0
- prune_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- prune_cli-0.1.0.dist-info/top_level.txt +1 -0
prune/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""prune - A CLI tool for cleaning Python code."""
|
prune/main.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Main entry point for the prune CLI."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import libcst as cst
|
|
8
|
+
|
|
9
|
+
from prune.transformers import (
|
|
10
|
+
AssertRemover,
|
|
11
|
+
DocstringRemover,
|
|
12
|
+
HeaderCommentRemover,
|
|
13
|
+
InlineCommentRemover,
|
|
14
|
+
LeadingCommentRemover,
|
|
15
|
+
LogRemover,
|
|
16
|
+
PrintRemover,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_python_files(start_dir: str) -> list[str]:
|
|
21
|
+
"""Recursively find all .py files, excluding specified directories."""
|
|
22
|
+
skip_dirs = {".git", ".venv", "venv", "__pycache__"}
|
|
23
|
+
python_files = []
|
|
24
|
+
|
|
25
|
+
for root, dirs, files in os.walk(start_dir):
|
|
26
|
+
# Remove skip directories from in-place traversal
|
|
27
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs]
|
|
28
|
+
|
|
29
|
+
for file in files:
|
|
30
|
+
if file.endswith(".py"):
|
|
31
|
+
python_files.append(os.path.join(root, file))
|
|
32
|
+
|
|
33
|
+
return python_files
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def process_file(
|
|
37
|
+
file_path: str,
|
|
38
|
+
remove_prints: bool = False,
|
|
39
|
+
remove_comments: bool = False,
|
|
40
|
+
remove_docstrings: bool = False,
|
|
41
|
+
remove_asserts: bool = False,
|
|
42
|
+
remove_logs: bool = False,
|
|
43
|
+
comment_options: dict = None,
|
|
44
|
+
log_levels: set[str] = None,
|
|
45
|
+
) -> tuple[int, int, int, int, int]:
|
|
46
|
+
"""Process a single file and return counts of removed items."""
|
|
47
|
+
prints_removed = 0
|
|
48
|
+
comments_removed = 0
|
|
49
|
+
docstrings_removed = 0
|
|
50
|
+
asserts_removed = 0
|
|
51
|
+
logs_removed = 0
|
|
52
|
+
|
|
53
|
+
if comment_options is None:
|
|
54
|
+
comment_options = {}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with open(file_path, encoding="utf-8") as f:
|
|
58
|
+
source_code = f.read()
|
|
59
|
+
|
|
60
|
+
module = cst.parse_module(source_code)
|
|
61
|
+
|
|
62
|
+
if remove_prints:
|
|
63
|
+
print_remover = PrintRemover()
|
|
64
|
+
module = module.visit(print_remover)
|
|
65
|
+
prints_removed = print_remover.removed_count
|
|
66
|
+
|
|
67
|
+
if remove_comments:
|
|
68
|
+
# Handle different comment removal options
|
|
69
|
+
if comment_options.get("all", False):
|
|
70
|
+
# Remove all types of comments
|
|
71
|
+
inline_remover = InlineCommentRemover(remove_all=True)
|
|
72
|
+
leading_remover = LeadingCommentRemover()
|
|
73
|
+
header_remover = HeaderCommentRemover()
|
|
74
|
+
|
|
75
|
+
module = module.visit(inline_remover)
|
|
76
|
+
comments_removed += inline_remover.removed_count
|
|
77
|
+
|
|
78
|
+
module = module.visit(leading_remover)
|
|
79
|
+
comments_removed += leading_remover.removed_count
|
|
80
|
+
|
|
81
|
+
module = module.visit(header_remover)
|
|
82
|
+
comments_removed += header_remover.removed_count
|
|
83
|
+
else:
|
|
84
|
+
# Handle individual comment types
|
|
85
|
+
if comment_options.get("inline", False):
|
|
86
|
+
inline_remover = InlineCommentRemover(remove_all=True)
|
|
87
|
+
module = module.visit(inline_remover)
|
|
88
|
+
comments_removed += inline_remover.removed_count
|
|
89
|
+
|
|
90
|
+
if comment_options.get("leading", False):
|
|
91
|
+
leading_remover = LeadingCommentRemover()
|
|
92
|
+
module = module.visit(leading_remover)
|
|
93
|
+
comments_removed += leading_remover.removed_count
|
|
94
|
+
|
|
95
|
+
if comment_options.get("header", False):
|
|
96
|
+
header_remover = HeaderCommentRemover()
|
|
97
|
+
module = module.visit(header_remover)
|
|
98
|
+
comments_removed += header_remover.removed_count
|
|
99
|
+
|
|
100
|
+
if comment_options.get("default", False):
|
|
101
|
+
inline_remover = InlineCommentRemover(remove_all=False)
|
|
102
|
+
module = module.visit(inline_remover)
|
|
103
|
+
comments_removed += inline_remover.removed_count
|
|
104
|
+
|
|
105
|
+
if remove_docstrings:
|
|
106
|
+
docstring_remover = DocstringRemover()
|
|
107
|
+
module = module.visit(docstring_remover)
|
|
108
|
+
docstrings_removed = docstring_remover.removed_count
|
|
109
|
+
|
|
110
|
+
if remove_asserts:
|
|
111
|
+
assert_remover = AssertRemover()
|
|
112
|
+
module = module.visit(assert_remover)
|
|
113
|
+
asserts_removed = assert_remover.removed_count
|
|
114
|
+
|
|
115
|
+
if remove_logs:
|
|
116
|
+
log_remover = LogRemover(log_levels)
|
|
117
|
+
module = module.visit(log_remover)
|
|
118
|
+
logs_removed = log_remover.removed_count
|
|
119
|
+
|
|
120
|
+
# Write back to file if any changes were made
|
|
121
|
+
if (
|
|
122
|
+
prints_removed > 0
|
|
123
|
+
or comments_removed > 0
|
|
124
|
+
or docstrings_removed > 0
|
|
125
|
+
or asserts_removed > 0
|
|
126
|
+
or logs_removed > 0
|
|
127
|
+
):
|
|
128
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
129
|
+
f.write(module.code)
|
|
130
|
+
|
|
131
|
+
except Exception:
|
|
132
|
+
# Skip files that can't be parsed or written
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
prints_removed,
|
|
137
|
+
comments_removed,
|
|
138
|
+
docstrings_removed,
|
|
139
|
+
asserts_removed,
|
|
140
|
+
logs_removed,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main() -> int:
|
|
145
|
+
"""Main entry point for the prune CLI."""
|
|
146
|
+
parser = argparse.ArgumentParser(
|
|
147
|
+
description="Remove print statements, comments, docstrings, asserts, and logs from Python files."
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"command",
|
|
151
|
+
choices=["comments", "prints", "docstrings", "asserts", "logs"],
|
|
152
|
+
help="What to remove",
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--quiet",
|
|
156
|
+
"-q",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help="Suppress per-file output (verbose is default)",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Comment-specific options
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--inline",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Remove all inline comments, including noqa, type, pragma",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--leading", action="store_true", help="Remove standalone/full-line comments"
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--header", action="store_true", help="Remove shebang & coding comments"
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--default",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="Remove inline comments only (preserve noqa, type:, pragma)",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Log-specific options
|
|
180
|
+
parser.add_argument("--trace", action="store_true", help="Remove trace level logs")
|
|
181
|
+
parser.add_argument("--debug", action="store_true", help="Remove debug level logs")
|
|
182
|
+
parser.add_argument("--info", action="store_true", help="Remove info level logs")
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--warning", action="store_true", help="Remove warning level logs"
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--success", action="store_true", help="Remove success level logs"
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument("--error", action="store_true", help="Remove error level logs")
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--exception", action="store_true", help="Remove exception level logs"
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"--critical", action="store_true", help="Remove critical level logs"
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"--all",
|
|
198
|
+
action="store_true",
|
|
199
|
+
help="Remove all types (for comments) or all log levels (for logs)",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
args = parser.parse_args()
|
|
203
|
+
|
|
204
|
+
# Validate comment options
|
|
205
|
+
if args.command == "comments":
|
|
206
|
+
comment_flags = {
|
|
207
|
+
"inline": args.inline,
|
|
208
|
+
"leading": args.leading,
|
|
209
|
+
"header": args.header,
|
|
210
|
+
"default": args.default,
|
|
211
|
+
"all": args.all,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Require at least one flag
|
|
215
|
+
if not any(comment_flags.values()):
|
|
216
|
+
print("Error: prune comments requires at least one flag")
|
|
217
|
+
print("Available flags:")
|
|
218
|
+
print(
|
|
219
|
+
" --default Remove inline comments only (preserve noqa, type:, pragma)"
|
|
220
|
+
)
|
|
221
|
+
print(
|
|
222
|
+
" --inline Remove all inline comments, including noqa, type, pragma"
|
|
223
|
+
)
|
|
224
|
+
print(" --leading Remove standalone/full-line comments")
|
|
225
|
+
print(" --header Remove shebang & coding comments")
|
|
226
|
+
print(" --all Remove all types of comments")
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
# Validate log options
|
|
230
|
+
if args.command == "logs":
|
|
231
|
+
log_flags = {
|
|
232
|
+
"trace": args.trace,
|
|
233
|
+
"debug": args.debug,
|
|
234
|
+
"info": args.info,
|
|
235
|
+
"warning": args.warning,
|
|
236
|
+
"success": args.success,
|
|
237
|
+
"error": args.error,
|
|
238
|
+
"exception": args.exception,
|
|
239
|
+
"critical": args.critical,
|
|
240
|
+
"all": args.all,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Require at least one flag
|
|
244
|
+
if not any(log_flags.values()):
|
|
245
|
+
print("Error: prune logs requires at least one flag")
|
|
246
|
+
print("Available flags:")
|
|
247
|
+
print(" --trace Remove trace level logs")
|
|
248
|
+
print(" --debug Remove debug level logs")
|
|
249
|
+
print(" --info Remove info level logs")
|
|
250
|
+
print(" --warning Remove warning level logs")
|
|
251
|
+
print(" --success Remove success level logs")
|
|
252
|
+
print(" --error Remove error level logs")
|
|
253
|
+
print(" --exception Remove exception level logs")
|
|
254
|
+
print(" --critical Remove critical level logs")
|
|
255
|
+
print(" --all Remove all log levels")
|
|
256
|
+
return 1
|
|
257
|
+
|
|
258
|
+
# Determine what to remove
|
|
259
|
+
remove_prints = args.command == "prints"
|
|
260
|
+
remove_comments = args.command == "comments"
|
|
261
|
+
remove_docstrings = args.command == "docstrings"
|
|
262
|
+
remove_asserts = args.command == "asserts"
|
|
263
|
+
remove_logs = args.command == "logs"
|
|
264
|
+
|
|
265
|
+
# Build comment options
|
|
266
|
+
comment_options = {}
|
|
267
|
+
if remove_comments:
|
|
268
|
+
# If --all is specified, ignore other flags
|
|
269
|
+
if args.all:
|
|
270
|
+
comment_options["all"] = True
|
|
271
|
+
else:
|
|
272
|
+
if args.inline:
|
|
273
|
+
comment_options["inline"] = True
|
|
274
|
+
if args.leading:
|
|
275
|
+
comment_options["leading"] = True
|
|
276
|
+
if args.header:
|
|
277
|
+
comment_options["header"] = True
|
|
278
|
+
if args.default:
|
|
279
|
+
comment_options["default"] = True
|
|
280
|
+
|
|
281
|
+
# Build log levels
|
|
282
|
+
log_levels = None
|
|
283
|
+
if remove_logs:
|
|
284
|
+
log_flags = {
|
|
285
|
+
"trace": args.trace,
|
|
286
|
+
"debug": args.debug,
|
|
287
|
+
"info": args.info,
|
|
288
|
+
"warning": args.warning,
|
|
289
|
+
"success": args.success,
|
|
290
|
+
"error": args.error,
|
|
291
|
+
"exception": args.exception,
|
|
292
|
+
"critical": args.critical,
|
|
293
|
+
"all": args.all,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# If --all is specified, ignore other flags and use all log levels
|
|
297
|
+
if args.all:
|
|
298
|
+
log_levels = {
|
|
299
|
+
"trace",
|
|
300
|
+
"debug",
|
|
301
|
+
"info",
|
|
302
|
+
"warning",
|
|
303
|
+
"success",
|
|
304
|
+
"error",
|
|
305
|
+
"exception",
|
|
306
|
+
"critical",
|
|
307
|
+
}
|
|
308
|
+
# If any specific log level flags are specified, only remove those levels
|
|
309
|
+
elif any(log_flags.values()):
|
|
310
|
+
log_levels = {
|
|
311
|
+
level
|
|
312
|
+
for level, enabled in log_flags.items()
|
|
313
|
+
if enabled and level != "all"
|
|
314
|
+
}
|
|
315
|
+
# No need for default case since validation requires at least one flag
|
|
316
|
+
|
|
317
|
+
# Find all Python files
|
|
318
|
+
python_files = find_python_files(".")
|
|
319
|
+
|
|
320
|
+
total_prints_removed = 0
|
|
321
|
+
total_comments_removed = 0
|
|
322
|
+
total_docstrings_removed = 0
|
|
323
|
+
total_asserts_removed = 0
|
|
324
|
+
total_logs_removed = 0
|
|
325
|
+
files_with_prints = 0
|
|
326
|
+
files_with_comments = 0
|
|
327
|
+
files_with_docstrings = 0
|
|
328
|
+
files_with_asserts = 0
|
|
329
|
+
files_with_logs = 0
|
|
330
|
+
|
|
331
|
+
# Verbose is default, quiet mode suppresses per-file output
|
|
332
|
+
verbose_mode = not args.quiet
|
|
333
|
+
|
|
334
|
+
# Process each file
|
|
335
|
+
for file_path in python_files:
|
|
336
|
+
(
|
|
337
|
+
prints_removed,
|
|
338
|
+
comments_removed,
|
|
339
|
+
docstrings_removed,
|
|
340
|
+
asserts_removed,
|
|
341
|
+
logs_removed,
|
|
342
|
+
) = process_file(
|
|
343
|
+
file_path,
|
|
344
|
+
remove_prints,
|
|
345
|
+
remove_comments,
|
|
346
|
+
remove_docstrings,
|
|
347
|
+
remove_asserts,
|
|
348
|
+
remove_logs,
|
|
349
|
+
comment_options,
|
|
350
|
+
log_levels,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if prints_removed > 0:
|
|
354
|
+
files_with_prints += 1
|
|
355
|
+
total_prints_removed += prints_removed
|
|
356
|
+
if verbose_mode:
|
|
357
|
+
relative_path = os.path.relpath(file_path, ".")
|
|
358
|
+
print(
|
|
359
|
+
f"{prints_removed} prints removed\t\t\033[90m{relative_path}\033[0m"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if comments_removed > 0:
|
|
363
|
+
files_with_comments += 1
|
|
364
|
+
total_comments_removed += comments_removed
|
|
365
|
+
if verbose_mode:
|
|
366
|
+
relative_path = os.path.relpath(file_path, ".")
|
|
367
|
+
print(
|
|
368
|
+
f"{comments_removed} comments removed\t\t\033[90m{relative_path}\033[0m"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if docstrings_removed > 0:
|
|
372
|
+
files_with_docstrings += 1
|
|
373
|
+
total_docstrings_removed += docstrings_removed
|
|
374
|
+
if verbose_mode:
|
|
375
|
+
relative_path = os.path.relpath(file_path, ".")
|
|
376
|
+
print(
|
|
377
|
+
f"{docstrings_removed} docstrings removed\t\t\033[90m{relative_path}\033[0m"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if asserts_removed > 0:
|
|
381
|
+
files_with_asserts += 1
|
|
382
|
+
total_asserts_removed += asserts_removed
|
|
383
|
+
if verbose_mode:
|
|
384
|
+
relative_path = os.path.relpath(file_path, ".")
|
|
385
|
+
print(
|
|
386
|
+
f"{asserts_removed} asserts removed\t\t\033[90m{relative_path}\033[0m"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if logs_removed > 0:
|
|
390
|
+
files_with_logs += 1
|
|
391
|
+
total_logs_removed += logs_removed
|
|
392
|
+
if verbose_mode:
|
|
393
|
+
relative_path = os.path.relpath(file_path, ".")
|
|
394
|
+
print(f"{logs_removed} logs removed\t\t\033[90m{relative_path}\033[0m")
|
|
395
|
+
|
|
396
|
+
# Print summary
|
|
397
|
+
if remove_prints:
|
|
398
|
+
print(f"{total_prints_removed} prints removed from {files_with_prints} files")
|
|
399
|
+
if remove_comments:
|
|
400
|
+
print(
|
|
401
|
+
f"{total_comments_removed} comments removed from {files_with_comments} files"
|
|
402
|
+
)
|
|
403
|
+
if remove_docstrings:
|
|
404
|
+
print(
|
|
405
|
+
f"{total_docstrings_removed} docstrings removed from {files_with_docstrings} files"
|
|
406
|
+
)
|
|
407
|
+
if remove_asserts:
|
|
408
|
+
print(
|
|
409
|
+
f"{total_asserts_removed} asserts removed from {files_with_asserts} files"
|
|
410
|
+
)
|
|
411
|
+
if remove_logs:
|
|
412
|
+
print(f"{total_logs_removed} logs removed from {files_with_logs} files")
|
|
413
|
+
|
|
414
|
+
return 0
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
if __name__ == "__main__":
|
|
418
|
+
sys.exit(main())
|
prune/transformers.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""LibCST transformers for removing print statements and comments."""
|
|
2
|
+
|
|
3
|
+
import libcst as cst
|
|
4
|
+
from libcst import matchers as m
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def remove_statement_preserve_comments(
|
|
8
|
+
node: cst.SimpleStatementLine,
|
|
9
|
+
) -> cst.FlattenSentinel:
|
|
10
|
+
"""Remove a statement but move its leading comments forward."""
|
|
11
|
+
new_nodes: list[cst.CSTNode] = []
|
|
12
|
+
|
|
13
|
+
for line in node.leading_lines:
|
|
14
|
+
new_nodes.append(line)
|
|
15
|
+
|
|
16
|
+
return cst.FlattenSentinel(new_nodes)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PrintRemover(cst.CSTTransformer):
|
|
20
|
+
"""Transformer to remove standalone print() statements."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.removed_count = 0
|
|
24
|
+
|
|
25
|
+
def leave_SimpleStatementLine(
|
|
26
|
+
self,
|
|
27
|
+
original_node: cst.SimpleStatementLine,
|
|
28
|
+
updated_node: cst.SimpleStatementLine,
|
|
29
|
+
):
|
|
30
|
+
if (
|
|
31
|
+
len(updated_node.body) == 1
|
|
32
|
+
and isinstance(updated_node.body[0], cst.Expr)
|
|
33
|
+
and m.matches(
|
|
34
|
+
updated_node.body[0].value,
|
|
35
|
+
m.Call(func=m.Name("print")),
|
|
36
|
+
)
|
|
37
|
+
):
|
|
38
|
+
self.removed_count += 1
|
|
39
|
+
return remove_statement_preserve_comments(original_node)
|
|
40
|
+
|
|
41
|
+
return updated_node
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InlineCommentRemover(cst.CSTTransformer):
|
|
45
|
+
"""Transformer to remove inline (trailing) comments only."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, remove_all: bool = False):
|
|
48
|
+
self.removed_count = 0
|
|
49
|
+
self.remove_all = remove_all
|
|
50
|
+
|
|
51
|
+
def leave_TrailingWhitespace(
|
|
52
|
+
self,
|
|
53
|
+
original_node: cst.TrailingWhitespace,
|
|
54
|
+
updated_node: cst.TrailingWhitespace,
|
|
55
|
+
) -> cst.TrailingWhitespace:
|
|
56
|
+
if updated_node.comment:
|
|
57
|
+
comment_text = updated_node.comment.value
|
|
58
|
+
|
|
59
|
+
if not self.remove_all and any(
|
|
60
|
+
keyword in comment_text for keyword in ["noqa", "type:", "pragma"]
|
|
61
|
+
):
|
|
62
|
+
return updated_node
|
|
63
|
+
|
|
64
|
+
self.removed_count += 1
|
|
65
|
+
return updated_node.with_changes(comment=None)
|
|
66
|
+
|
|
67
|
+
return updated_node
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LeadingCommentRemover(cst.CSTTransformer):
|
|
71
|
+
"""Transformer to remove standalone/full-line comments."""
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
self.removed_count = 0
|
|
75
|
+
|
|
76
|
+
def leave_Module(
|
|
77
|
+
self,
|
|
78
|
+
original_node: cst.Module,
|
|
79
|
+
updated_node: cst.Module,
|
|
80
|
+
) -> cst.Module:
|
|
81
|
+
if not updated_node.leading_lines:
|
|
82
|
+
return updated_node
|
|
83
|
+
|
|
84
|
+
new_lines: list[cst.EmptyLine] = []
|
|
85
|
+
|
|
86
|
+
for line in updated_node.leading_lines:
|
|
87
|
+
if isinstance(line, cst.EmptyLine) and line.comment:
|
|
88
|
+
self.removed_count += 1
|
|
89
|
+
continue
|
|
90
|
+
new_lines.append(line)
|
|
91
|
+
|
|
92
|
+
return updated_node.with_changes(leading_lines=new_lines)
|
|
93
|
+
|
|
94
|
+
def leave_EmptyLine(
|
|
95
|
+
self,
|
|
96
|
+
original_node: cst.EmptyLine,
|
|
97
|
+
updated_node: cst.EmptyLine,
|
|
98
|
+
) -> cst.EmptyLine:
|
|
99
|
+
if updated_node.comment:
|
|
100
|
+
self.removed_count += 1
|
|
101
|
+
return updated_node.with_changes(comment=None)
|
|
102
|
+
|
|
103
|
+
return updated_node
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class HeaderCommentRemover(cst.CSTTransformer):
|
|
107
|
+
"""Transformer to remove shebang and coding comments."""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
self.removed_count = 0
|
|
111
|
+
|
|
112
|
+
def leave_Module(
|
|
113
|
+
self,
|
|
114
|
+
original_node: cst.Module,
|
|
115
|
+
updated_node: cst.Module,
|
|
116
|
+
) -> cst.Module:
|
|
117
|
+
if not updated_node.leading_lines:
|
|
118
|
+
return updated_node
|
|
119
|
+
|
|
120
|
+
new_lines: list[cst.EmptyLine] = []
|
|
121
|
+
|
|
122
|
+
for line in updated_node.leading_lines:
|
|
123
|
+
if isinstance(line, cst.EmptyLine) and line.comment:
|
|
124
|
+
comment_text = line.comment.value.strip().lower()
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
comment_text.startswith("#!")
|
|
128
|
+
or "coding" in comment_text
|
|
129
|
+
or comment_text.startswith("# vim:")
|
|
130
|
+
):
|
|
131
|
+
self.removed_count += 1
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
new_lines.append(line)
|
|
135
|
+
|
|
136
|
+
return updated_node.with_changes(leading_lines=new_lines)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DocstringRemover(cst.CSTTransformer):
|
|
140
|
+
"""Transformer to remove docstrings."""
|
|
141
|
+
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self.removed_count = 0
|
|
144
|
+
|
|
145
|
+
def _strip_docstring(
|
|
146
|
+
self,
|
|
147
|
+
body: list[cst.BaseStatement],
|
|
148
|
+
) -> list[cst.BaseStatement]:
|
|
149
|
+
if (
|
|
150
|
+
body
|
|
151
|
+
and isinstance(body[0], cst.SimpleStatementLine)
|
|
152
|
+
and len(body[0].body) == 1
|
|
153
|
+
and isinstance(body[0].body[0], cst.Expr)
|
|
154
|
+
and isinstance(body[0].body[0].value, cst.SimpleString)
|
|
155
|
+
):
|
|
156
|
+
self.removed_count += 1
|
|
157
|
+
return body[1:]
|
|
158
|
+
|
|
159
|
+
return body
|
|
160
|
+
|
|
161
|
+
def leave_Module(
|
|
162
|
+
self,
|
|
163
|
+
original_node: cst.Module,
|
|
164
|
+
updated_node: cst.Module,
|
|
165
|
+
) -> cst.Module:
|
|
166
|
+
return updated_node.with_changes(
|
|
167
|
+
body=self._strip_docstring(list(updated_node.body)),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def leave_ClassDef(
|
|
171
|
+
self,
|
|
172
|
+
original_node: cst.ClassDef,
|
|
173
|
+
updated_node: cst.ClassDef,
|
|
174
|
+
) -> cst.ClassDef:
|
|
175
|
+
new_body = updated_node.body.with_changes(
|
|
176
|
+
body=self._strip_docstring(list(updated_node.body.body)),
|
|
177
|
+
)
|
|
178
|
+
return updated_node.with_changes(body=new_body)
|
|
179
|
+
|
|
180
|
+
def leave_FunctionDef(
|
|
181
|
+
self,
|
|
182
|
+
original_node: cst.FunctionDef,
|
|
183
|
+
updated_node: cst.FunctionDef,
|
|
184
|
+
) -> cst.FunctionDef:
|
|
185
|
+
new_body = updated_node.body.with_changes(
|
|
186
|
+
body=self._strip_docstring(list(updated_node.body.body)),
|
|
187
|
+
)
|
|
188
|
+
return updated_node.with_changes(body=new_body)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AssertRemover(cst.CSTTransformer):
|
|
192
|
+
"""Transformer to remove assert statements."""
|
|
193
|
+
|
|
194
|
+
def __init__(self):
|
|
195
|
+
self.removed_count = 0
|
|
196
|
+
|
|
197
|
+
def leave_SimpleStatementLine(
|
|
198
|
+
self,
|
|
199
|
+
original_node: cst.SimpleStatementLine,
|
|
200
|
+
updated_node: cst.SimpleStatementLine,
|
|
201
|
+
):
|
|
202
|
+
if len(updated_node.body) == 1 and isinstance(updated_node.body[0], cst.Assert):
|
|
203
|
+
self.removed_count += 1
|
|
204
|
+
return remove_statement_preserve_comments(original_node)
|
|
205
|
+
|
|
206
|
+
return updated_node
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class LogRemover(cst.CSTTransformer):
|
|
210
|
+
"""Transformer to remove logging statements."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, log_levels: set[str] | None = None):
|
|
213
|
+
self.removed_count = 0
|
|
214
|
+
self.log_levels = log_levels or {
|
|
215
|
+
"trace",
|
|
216
|
+
"debug",
|
|
217
|
+
"info",
|
|
218
|
+
"warning",
|
|
219
|
+
"success",
|
|
220
|
+
"error",
|
|
221
|
+
"exception",
|
|
222
|
+
"critical",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def leave_SimpleStatementLine(
|
|
226
|
+
self,
|
|
227
|
+
original_node: cst.SimpleStatementLine,
|
|
228
|
+
updated_node: cst.SimpleStatementLine,
|
|
229
|
+
):
|
|
230
|
+
if (
|
|
231
|
+
len(updated_node.body) == 1
|
|
232
|
+
and isinstance(updated_node.body[0], cst.Expr)
|
|
233
|
+
and isinstance(updated_node.body[0].value, cst.Call)
|
|
234
|
+
and isinstance(updated_node.body[0].value.func, cst.Attribute)
|
|
235
|
+
):
|
|
236
|
+
attr = updated_node.body[0].value.func
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
isinstance(attr.attr, cst.Name)
|
|
240
|
+
and attr.attr.value in self.log_levels
|
|
241
|
+
and isinstance(attr.value, cst.Name)
|
|
242
|
+
and attr.value.value in {"log", "logger", "logging"}
|
|
243
|
+
):
|
|
244
|
+
self.removed_count += 1
|
|
245
|
+
return remove_statement_preserve_comments(original_node)
|
|
246
|
+
|
|
247
|
+
return updated_node
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prune-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI tool named prune for cleaning Python code
|
|
5
|
+
Author-email: vincentdeneuf <0189vn@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vincentdeneuf/prune-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/vincentdeneuf/prune-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/vincentdeneuf/prune-cli/issues
|
|
10
|
+
Keywords: cli,code,formatting,refactor,ast,prune
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: libcst
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# prune
|
|
27
|
+
|
|
28
|
+
A CLI tool named prune for cleaning Python code.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install prune-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
prune <command> [options]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Commands
|
|
43
|
+
|
|
44
|
+
- `prune comments` - Remove comments from Python files
|
|
45
|
+
- `prune prints` - Remove print statements from Python files
|
|
46
|
+
- `prune docstrings` - Remove docstrings from Python files
|
|
47
|
+
- `prune asserts` - Remove assert statements from Python files
|
|
48
|
+
- `prune logs` - Remove logging statements from Python files
|
|
49
|
+
|
|
50
|
+
### Examples
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Remove all print statements
|
|
54
|
+
prune prints
|
|
55
|
+
|
|
56
|
+
# Remove inline comments only (preserve noqa, type:, pragma)
|
|
57
|
+
prune comments --default
|
|
58
|
+
|
|
59
|
+
# Remove all types of comments
|
|
60
|
+
prune comments --all
|
|
61
|
+
|
|
62
|
+
# Remove specific log levels
|
|
63
|
+
prune logs --debug --info --error
|
|
64
|
+
|
|
65
|
+
# Remove all log levels
|
|
66
|
+
prune logs --all
|
|
67
|
+
|
|
68
|
+
# Show per-file details (verbose is default)
|
|
69
|
+
prune prints
|
|
70
|
+
|
|
71
|
+
# Suppress per-file output
|
|
72
|
+
prune prints --quiet
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
This project uses a src-layout packaging structure.
|
|
78
|
+
|
|
79
|
+
### Requirements
|
|
80
|
+
|
|
81
|
+
- Python >= 3.11
|
|
82
|
+
- LibCST
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT License
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
prune/__init__.py,sha256=FPNPkMGSrTJKVOzqLUPuXm_CANnYzFwVIL8Nwl5xa5c,52
|
|
2
|
+
prune/main.py,sha256=Y1kKiB3TJELrXh5M3__9KHm5PRMVsaQtaKrhodkIn6Q,14499
|
|
3
|
+
prune/transformers.py,sha256=Dk26A2ncWpcfeoLmongv6N0QFUFtdddOXj1Iygp84Zw,7466
|
|
4
|
+
prune_cli-0.1.0.dist-info/licenses/LICENSE,sha256=XKKSDU9WlUEAyPNlRhq6e2xhVNpJc097JwPZJ1rUnRE,1077
|
|
5
|
+
prune_cli-0.1.0.dist-info/METADATA,sha256=EiQ81nJ82PY6XOirfrL3cyvssJOMpND9W2QI5_Ava7o,2160
|
|
6
|
+
prune_cli-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
7
|
+
prune_cli-0.1.0.dist-info/entry_points.txt,sha256=0P9nFJ9URKjgHQEiF61_2_Fb-MDj1wBLkYGQOPi4mD4,42
|
|
8
|
+
prune_cli-0.1.0.dist-info/top_level.txt,sha256=pIjROT6gT2MV16IHIiJnaAFGhOcmcji6omu6LqwRfMQ,6
|
|
9
|
+
prune_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune
|