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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ prune = prune.main:main
@@ -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