crpy-tools 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Youness
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,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: crpy-tools
3
+ Version: 0.1.0
4
+ Summary: A CLI and GUI tool built with PySide6
5
+ Author-email: Youness <bouness@live.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Youness
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/bouness/crpy_tools
29
+ Project-URL: Repository, https://github.com/bouness/crpy_tools
30
+ Requires-Python: >=3.8
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: PySide6
34
+ Dynamic: license-file
35
+
36
+ # crpy-tools - Comment Removal Tools for Python
37
+
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ crpy-tools is a powerful tool for removing comments from Python files while preserving code structure and indentation. It offers both command-line interface (CLI) and graphical user interface (GUI) options for maximum flexibility.
41
+
42
+ ## Features
43
+
44
+ - 🚀 Remove all types of Python comments (#-style)
45
+ - 📝 Optionally remove docstrings
46
+ - 📁 Process single files or entire directories
47
+ - 🔁 Recursive directory processing
48
+ - 🎨 Clean code formatting with preserved indentation
49
+ - 💻 CLI for scripting and automation
50
+ - 🖥️ GUI for easy interactive use
51
+ - 📊 Progress reporting and logging
52
+
53
+ ## Installation
54
+
55
+ ### Requirements
56
+ - Python 3.7+
57
+ - PySide6 (for GUI)
58
+
59
+ ### Install from PyPI (coming soon)
60
+ ```bash
61
+ pip install crpy-tools
@@ -0,0 +1,26 @@
1
+ # crpy-tools - Comment Removal Tools for Python
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ crpy-tools is a powerful tool for removing comments from Python files while preserving code structure and indentation. It offers both command-line interface (CLI) and graphical user interface (GUI) options for maximum flexibility.
6
+
7
+ ## Features
8
+
9
+ - 🚀 Remove all types of Python comments (#-style)
10
+ - 📝 Optionally remove docstrings
11
+ - 📁 Process single files or entire directories
12
+ - 🔁 Recursive directory processing
13
+ - 🎨 Clean code formatting with preserved indentation
14
+ - 💻 CLI for scripting and automation
15
+ - 🖥️ GUI for easy interactive use
16
+ - 📊 Progress reporting and logging
17
+
18
+ ## Installation
19
+
20
+ ### Requirements
21
+ - Python 3.7+
22
+ - PySide6 (for GUI)
23
+
24
+ ### Install from PyPI (coming soon)
25
+ ```bash
26
+ pip install crpy-tools
File without changes
@@ -0,0 +1,712 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Python Comment Remover
4
+ Removes all comments from Python files while preserving indentation and code structure.
5
+ """
6
+
7
+ import re
8
+ import sys
9
+ import argparse
10
+ import os
11
+ from pathlib import Path
12
+ from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
13
+ QLabel, QLineEdit, QPushButton, QTextEdit, QCheckBox, QGroupBox,
14
+ QFileDialog, QProgressBar, QStatusBar, QMessageBox)
15
+ from PySide6.QtCore import Qt, QThread, Signal, QSettings
16
+ from PySide6.QtGui import QFont, QTextOption
17
+
18
+ class CommentRemoverWorker(QThread):
19
+ """Worker thread for processing files to avoid freezing the GUI"""
20
+ progress_signal = Signal(str, int)
21
+ finished_signal = Signal()
22
+ error_signal = Signal(str)
23
+
24
+ def __init__(self, input_path, output_path, suffix, remove_docstrings, recursive):
25
+ super().__init__()
26
+ self.input_path = Path(input_path)
27
+ self.output_path = Path(output_path) if output_path else None
28
+ self.suffix = suffix
29
+ self.remove_docstrings = remove_docstrings
30
+ self.recursive = recursive
31
+ self.cancel_requested = False
32
+
33
+ def run(self):
34
+ """Main processing method"""
35
+ try:
36
+ if self.input_path.is_file():
37
+ # Determine output path for single file
38
+ if self.output_path:
39
+ if self.output_path.is_dir():
40
+ output_file = self.output_path / self.input_path.name
41
+ else:
42
+ output_file = self.output_path
43
+ else:
44
+ # Add suffix to filename in same directory
45
+ output_file = self.input_path.parent / f"{self.input_path.stem}{self.suffix}{self.input_path.suffix}"
46
+
47
+ self.process_file(self.input_path, output_file, self.remove_docstrings)
48
+ self.progress_signal.emit(f"Processed: {self.input_path} -> {output_file}", 100)
49
+
50
+ elif self.input_path.is_dir():
51
+ pattern = '**/*.py' if self.recursive else '*.py'
52
+ files = list(self.input_path.glob(pattern))
53
+ total = len(files)
54
+
55
+ if total == 0:
56
+ self.error_signal.emit("No Python files found in the specified directory")
57
+ return
58
+
59
+ for i, py_file in enumerate(files):
60
+ if self.cancel_requested:
61
+ self.progress_signal.emit("Processing canceled", 0)
62
+ return
63
+
64
+ if self.output_path:
65
+ # Create output directory structure
66
+ rel_path = py_file.relative_to(self.input_path)
67
+ output_file = self.output_path / rel_path
68
+ output_file.parent.mkdir(parents=True, exist_ok=True)
69
+ else:
70
+ # Add suffix to filename in same directory
71
+ output_file = py_file.parent / f"{py_file.stem}{self.suffix}{py_file.suffix}"
72
+
73
+ self.process_file(py_file, output_file, self.remove_docstrings)
74
+ self.progress_signal.emit(f"Processed: {py_file}", int((i+1)/total*100))
75
+ else:
76
+ self.error_signal.emit(f"Error: {self.input_path} is not a valid file or directory")
77
+ except Exception as e:
78
+ self.error_signal.emit(f"Error during processing: {str(e)}")
79
+ finally:
80
+ self.finished_signal.emit()
81
+
82
+ def process_file(self, input_path, output_path, remove_docstrings):
83
+ """Process a single file with error handling"""
84
+ try:
85
+ with open(input_path, 'r', encoding='utf-8') as f:
86
+ content = f.read()
87
+
88
+ processed_content = remove_comments_from_text(content, remove_docstrings)
89
+
90
+ # Ensure output directory exists
91
+ output_path.parent.mkdir(parents=True, exist_ok=True)
92
+
93
+ with open(output_path, 'w', encoding='utf-8') as f:
94
+ f.write(processed_content)
95
+ except Exception as e:
96
+ self.error_signal.emit(f"Error processing {input_path}: {e}")
97
+
98
+ def cancel(self):
99
+ """Request cancellation of processing"""
100
+ self.cancel_requested = True
101
+
102
+
103
+ def remove_comments_from_line(line):
104
+ """
105
+ Remove comments from a single line while handling strings properly.
106
+ Preserves the original indentation.
107
+ """
108
+ # Track if we're inside a string
109
+ in_single_quote = False
110
+ in_double_quote = False
111
+ in_triple_single = False
112
+ in_triple_double = False
113
+ escaped = False
114
+ i = 0
115
+
116
+ while i < len(line):
117
+ char = line[i]
118
+
119
+ # Handle escape sequences
120
+ if escaped:
121
+ escaped = False
122
+ i += 1
123
+ continue
124
+
125
+ if char == '\\' and (in_single_quote or in_double_quote):
126
+ escaped = True
127
+ i += 1
128
+ continue
129
+
130
+ # Check for triple quotes first
131
+ if i <= len(line) - 3:
132
+ if line[i:i+3] == '"""':
133
+ if not in_single_quote and not in_triple_single:
134
+ in_triple_double = not in_triple_double
135
+ i += 3
136
+ continue
137
+ elif line[i:i+3] == "'''":
138
+ if not in_double_quote and not in_triple_double:
139
+ in_triple_single = not in_triple_single
140
+ i += 3
141
+ continue
142
+
143
+ # Handle single character quotes
144
+ if char == '"' and not in_single_quote and not in_triple_single and not in_triple_double:
145
+ in_double_quote = not in_double_quote
146
+ elif char == "'" and not in_double_quote and not in_triple_double and not in_triple_single:
147
+ in_single_quote = not in_single_quote
148
+ elif char == '#' and not (in_single_quote or in_double_quote or in_triple_single or in_triple_double):
149
+ # Found a comment outside of strings - remove everything from here
150
+ return line[:i].rstrip() + '\n' if line.endswith('\n') else line[:i].rstrip()
151
+
152
+ i += 1
153
+
154
+ return line
155
+
156
+ def is_docstring_line(line, prev_lines, remove_docstrings):
157
+ """
158
+ Check if a line is likely part of a docstring.
159
+ Simple heuristic: if it's a string literal at the beginning of a function/class/module.
160
+ """
161
+ if not remove_docstrings:
162
+ return False
163
+
164
+ stripped = line.strip()
165
+ if not (stripped.startswith('"""') or stripped.startswith("'''")):
166
+ return False
167
+
168
+ # If this is the very first content line (module docstring)
169
+ if not prev_lines or all(not prev_line.strip() for prev_line in prev_lines):
170
+ return True
171
+
172
+ # Look at previous non-empty, non-comment lines to see if this could be a docstring
173
+ code_lines_seen = 0
174
+ for prev_line in reversed(prev_lines):
175
+ prev_stripped = prev_line.strip()
176
+ if not prev_stripped or prev_stripped.startswith('#'):
177
+ continue
178
+
179
+ code_lines_seen += 1
180
+
181
+ # Check if previous line indicates start of function, class, or module
182
+ if (prev_stripped.startswith('def ') or
183
+ prev_stripped.startswith('class ') or
184
+ prev_stripped.startswith('async def ') or
185
+ prev_stripped.endswith(':')):
186
+ return True
187
+
188
+ # If this is the first real code line after imports/shebang, it could be module docstring
189
+ if code_lines_seen == 1 and (
190
+ prev_stripped.startswith('import ') or
191
+ prev_stripped.startswith('from ') or
192
+ prev_stripped.startswith('#!') # shebang
193
+ ):
194
+ return True
195
+
196
+ # If we hit other actual code, this is probably not a docstring
197
+ if code_lines_seen >= 2:
198
+ break
199
+
200
+ return False
201
+
202
+ def clean_excessive_whitespace(lines):
203
+ """
204
+ Remove unnecessary whitespace but add strategic blank lines for readability.
205
+ Add blank lines before: classes, functions, methods (except first in class).
206
+ """
207
+ if not lines:
208
+ return lines
209
+
210
+ cleaned = []
211
+
212
+ for i, line in enumerate(lines):
213
+ stripped = line.strip()
214
+
215
+ # Only process lines that have actual content
216
+ if not stripped:
217
+ continue
218
+
219
+ # Check if we need to add a blank line before this line
220
+ should_add_blank = False
221
+
222
+ if i > 0: # Don't add blank line before first line
223
+ # Add blank line before class definitions
224
+ if stripped.startswith('class '):
225
+ should_add_blank = True
226
+
227
+ # Add blank line before function definitions (but not methods immediately after class)
228
+ elif stripped.startswith('def ') or stripped.startswith('async def '):
229
+ # Check if this is a method (inside a class) or a standalone function
230
+ # Look at previous non-blank lines to determine context
231
+ prev_line_found = False
232
+ for j in range(i-1, -1, -1):
233
+ prev_stripped = lines[j].strip()
234
+ if prev_stripped:
235
+ prev_line_found = True
236
+ # If previous line is class definition, don't add blank (first method)
237
+ if prev_stripped.startswith('class '):
238
+ should_add_blank = False
239
+ else:
240
+ should_add_blank = True
241
+ break
242
+
243
+ if not prev_line_found:
244
+ should_add_blank = True
245
+
246
+ # Add blank line if needed
247
+ if should_add_blank:
248
+ cleaned.append('\n')
249
+
250
+ # Add the actual line
251
+ cleaned.append(line)
252
+
253
+ return cleaned
254
+
255
+ def remove_comments_from_text(text, remove_docstrings=False):
256
+ """
257
+ Remove comments from Python code text while preserving indentation.
258
+ Handles multi-line strings and various comment scenarios.
259
+ """
260
+ lines = text.splitlines(keepends=True)
261
+ result_lines = []
262
+ in_multiline_string = False
263
+ in_docstring = False
264
+ multiline_delimiter = None
265
+
266
+ for i, line in enumerate(lines):
267
+ stripped = line.strip()
268
+
269
+ # Skip empty lines entirely for minification
270
+ if not stripped:
271
+ continue
272
+
273
+ # Handle multi-line string/docstring detection
274
+ if not in_multiline_string and not in_docstring:
275
+ # Check if this line starts a multi-line string
276
+ for delimiter in ['"""', "'''"]:
277
+ if delimiter in line:
278
+ # Count occurrences to see if string is closed on same line
279
+ count = line.count(delimiter)
280
+ if count % 2 == 1: # Odd count means string is opened but not closed
281
+ # Check if this is a docstring
282
+ if is_docstring_line(line, result_lines, remove_docstrings):
283
+ in_docstring = True
284
+ else:
285
+ in_multiline_string = True
286
+ multiline_delimiter = delimiter
287
+ break
288
+
289
+ # Handle single-line docstrings
290
+ if remove_docstrings and not in_multiline_string and not in_docstring:
291
+ for delimiter in ['"""', "'''"]:
292
+ if line.count(delimiter) >= 2: # Single line docstring
293
+ if is_docstring_line(line, result_lines, remove_docstrings):
294
+ # Skip this line entirely
295
+ continue
296
+
297
+ elif in_docstring:
298
+ # We're in a docstring, check if it's closed
299
+ if multiline_delimiter in line:
300
+ count = line.count(multiline_delimiter)
301
+ if count % 2 == 1: # Odd count means string is closed
302
+ in_docstring = False
303
+ multiline_delimiter = None
304
+ # Skip docstring lines
305
+ continue
306
+
307
+ elif in_multiline_string:
308
+ # We're in a multi-line string, check if it's closed
309
+ if multiline_delimiter in line:
310
+ count = line.count(multiline_delimiter)
311
+ if count % 2 == 1: # Odd count means string is closed
312
+ in_multiline_string = False
313
+ multiline_delimiter = None
314
+ # Keep multi-line string content
315
+ result_lines.append(line)
316
+
317
+ # Process regular lines (not in docstring or multiline string)
318
+ if not in_multiline_string and not in_docstring:
319
+ # Process line for comment removal
320
+ processed_line = remove_comments_from_line(line)
321
+
322
+ # Only add lines that have content after processing
323
+ if processed_line.strip():
324
+ result_lines.append(processed_line)
325
+
326
+ # Add strategic blank lines for readability
327
+ result_lines = clean_excessive_whitespace(result_lines)
328
+
329
+ return ''.join(result_lines)
330
+
331
+
332
+ class PythonCommentRemoverGUI(QMainWindow):
333
+ """Main GUI window for the Python Comment Remover"""
334
+ def __init__(self):
335
+ super().__init__()
336
+ self.setWindowTitle("Python Comment Remover")
337
+ self.resize(800, 600)
338
+
339
+ # Load settings
340
+ self.settings = QSettings("PythonCommentRemover", "Settings")
341
+
342
+ # Create central widget and layout
343
+ central_widget = QWidget()
344
+ self.setCentralWidget(central_widget)
345
+ main_layout = QVBoxLayout(central_widget)
346
+ main_layout.setSpacing(15)
347
+
348
+ # Create input section
349
+ input_group = QGroupBox("Input Options")
350
+ input_layout = QVBoxLayout(input_group)
351
+
352
+ # Input file/directory selection
353
+ input_path_layout = QHBoxLayout()
354
+ input_path_layout.addWidget(QLabel("Input File/Directory:"))
355
+ self.input_path_edit = QLineEdit()
356
+ self.input_path_edit.setPlaceholderText("Select a file or directory...")
357
+ input_path_layout.addWidget(self.input_path_edit, 3)
358
+
359
+ self.browse_input_btn = QPushButton("Browse...")
360
+ self.browse_input_btn.clicked.connect(self.browse_input)
361
+ input_path_layout.addWidget(self.browse_input_btn)
362
+
363
+ input_layout.addLayout(input_path_layout)
364
+
365
+ # Output options
366
+ output_path_layout = QHBoxLayout()
367
+ output_path_layout.addWidget(QLabel("Output Directory:"))
368
+ self.output_path_edit = QLineEdit()
369
+ self.output_path_edit.setPlaceholderText("Select output directory (optional)...")
370
+ output_path_layout.addWidget(self.output_path_edit, 3)
371
+
372
+ self.browse_output_btn = QPushButton("Browse...")
373
+ self.browse_output_btn.clicked.connect(self.browse_output)
374
+ output_path_layout.addWidget(self.browse_output_btn)
375
+
376
+ input_layout.addLayout(output_path_layout)
377
+
378
+ # Suffix option
379
+ suffix_layout = QHBoxLayout()
380
+ suffix_layout.addWidget(QLabel("Output File Suffix:"))
381
+ self.suffix_edit = QLineEdit("_no_comments")
382
+ suffix_layout.addWidget(self.suffix_edit)
383
+ input_layout.addLayout(suffix_layout)
384
+
385
+ # Options checkboxes
386
+ options_layout = QHBoxLayout()
387
+ self.recursive_cb = QCheckBox("Process Subdirectories")
388
+ self.remove_docstrings_cb = QCheckBox("Remove Docstrings")
389
+ options_layout.addWidget(self.recursive_cb)
390
+ options_layout.addWidget(self.remove_docstrings_cb)
391
+ input_layout.addLayout(options_layout)
392
+
393
+ main_layout.addWidget(input_group)
394
+
395
+ # Create action buttons
396
+ button_layout = QHBoxLayout()
397
+ self.run_btn = QPushButton("Run Processing")
398
+ self.run_btn.setFixedHeight(40)
399
+ self.run_btn.clicked.connect(self.start_processing)
400
+ button_layout.addWidget(self.run_btn)
401
+
402
+ self.cancel_btn = QPushButton("Cancel")
403
+ self.cancel_btn.setFixedHeight(40)
404
+ self.cancel_btn.setEnabled(False)
405
+ self.cancel_btn.clicked.connect(self.cancel_processing)
406
+ button_layout.addWidget(self.cancel_btn)
407
+
408
+ main_layout.addLayout(button_layout)
409
+
410
+ # Progress bar
411
+ self.progress_bar = QProgressBar()
412
+ self.progress_bar.setRange(0, 100)
413
+ self.progress_bar.setTextVisible(True)
414
+ main_layout.addWidget(self.progress_bar)
415
+
416
+ # Log output
417
+ log_group = QGroupBox("Processing Log")
418
+ log_layout = QVBoxLayout(log_group)
419
+ self.log_text = QTextEdit()
420
+ self.log_text.setReadOnly(True)
421
+ self.log_text.setFont(QFont("Courier New", 10))
422
+ self.log_text.setWordWrapMode(QTextOption.NoWrap)
423
+ log_layout.addWidget(self.log_text)
424
+
425
+ main_layout.addWidget(log_group, 1) # Give log area more space
426
+
427
+ # Status bar
428
+ self.status_bar = QStatusBar()
429
+ self.setStatusBar(self.status_bar)
430
+ self.status_bar.showMessage("Ready to process files")
431
+
432
+ # Initialize worker thread
433
+ self.worker_thread = None
434
+
435
+ # Load saved settings
436
+ self.load_settings()
437
+
438
+ def load_settings(self):
439
+ """Load saved settings from previous session"""
440
+ # Load input path
441
+ input_path = self.settings.value("input_path", "")
442
+ if input_path:
443
+ self.input_path_edit.setText(input_path)
444
+
445
+ # Load output path
446
+ output_path = self.settings.value("output_path", "")
447
+ if output_path:
448
+ self.output_path_edit.setText(output_path)
449
+
450
+ # Load suffix
451
+ suffix = self.settings.value("suffix", "_no_comments")
452
+ self.suffix_edit.setText(suffix)
453
+
454
+ # Load options
455
+ self.recursive_cb.setChecked(self.settings.value("recursive", False, type=bool))
456
+ self.remove_docstrings_cb.setChecked(self.settings.value("remove_docstrings", False, type=bool))
457
+
458
+ def save_settings(self):
459
+ """Save current settings for next session"""
460
+ self.settings.setValue("input_path", self.input_path_edit.text())
461
+ self.settings.setValue("output_path", self.output_path_edit.text())
462
+ self.settings.setValue("suffix", self.suffix_edit.text())
463
+ self.settings.setValue("recursive", self.recursive_cb.isChecked())
464
+ self.settings.setValue("remove_docstrings", self.remove_docstrings_cb.isChecked())
465
+
466
+ def closeEvent(self, event):
467
+ """Handle window close event"""
468
+ self.save_settings()
469
+ event.accept()
470
+
471
+ def browse_input(self):
472
+ """Open dialog to select input file or directory"""
473
+ path, _ = QFileDialog.getOpenFileName(
474
+ self,
475
+ "Select Python File",
476
+ self.input_path_edit.text() or "",
477
+ "Python Files (*.py);;All Files (*)"
478
+ )
479
+
480
+ if not path:
481
+ # If file dialog canceled, try directory dialog
482
+ path = QFileDialog.getExistingDirectory(
483
+ self,
484
+ "Select Directory",
485
+ self.input_path_edit.text() or ""
486
+ )
487
+
488
+ if path:
489
+ self.input_path_edit.setText(path)
490
+
491
+ def browse_output(self):
492
+ """Open dialog to select output directory"""
493
+ path = QFileDialog.getExistingDirectory(
494
+ self,
495
+ "Select Output Directory",
496
+ self.output_path_edit.text() or ""
497
+ )
498
+
499
+ if path:
500
+ self.output_path_edit.setText(path)
501
+
502
+ def start_processing(self):
503
+ """Start the comment removal process"""
504
+ input_path = self.input_path_edit.text().strip()
505
+ if not input_path:
506
+ QMessageBox.warning(self, "Input Required", "Please select an input file or directory.")
507
+ return
508
+
509
+ output_path = self.output_path_edit.text().strip() or None
510
+ suffix = self.suffix_edit.text().strip()
511
+ remove_docstrings = self.remove_docstrings_cb.isChecked()
512
+ recursive = self.recursive_cb.isChecked()
513
+
514
+ # Clear log
515
+ self.log_text.clear()
516
+ self.log_text.append("Starting processing...")
517
+ self.log_text.append("-" * 80)
518
+
519
+ # Validate output path if specified
520
+ if output_path and not os.path.exists(output_path):
521
+ try:
522
+ os.makedirs(output_path, exist_ok=True)
523
+ except Exception as e:
524
+ QMessageBox.critical(self, "Output Error", f"Cannot create output directory: {str(e)}")
525
+ return
526
+
527
+ # Create and start worker thread
528
+ self.worker_thread = CommentRemoverWorker(
529
+ input_path,
530
+ output_path,
531
+ suffix,
532
+ remove_docstrings,
533
+ recursive
534
+ )
535
+
536
+ # Connect signals
537
+ self.worker_thread.progress_signal.connect(self.update_progress)
538
+ self.worker_thread.finished_signal.connect(self.processing_finished)
539
+ self.worker_thread.error_signal.connect(self.handle_error)
540
+
541
+ # Update UI
542
+ self.run_btn.setEnabled(False)
543
+ self.cancel_btn.setEnabled(True)
544
+ self.status_bar.showMessage("Processing files...")
545
+ self.progress_bar.setValue(0)
546
+
547
+ # Start thread
548
+ self.worker_thread.start()
549
+
550
+ def cancel_processing(self):
551
+ """Request cancellation of the current processing"""
552
+ if self.worker_thread and self.worker_thread.isRunning():
553
+ self.worker_thread.cancel()
554
+ self.cancel_btn.setEnabled(False)
555
+ self.status_bar.showMessage("Canceling...")
556
+
557
+ def update_progress(self, message, progress):
558
+ """Update progress bar and log"""
559
+ self.log_text.append(message)
560
+ self.progress_bar.setValue(progress)
561
+ self.status_bar.showMessage(message)
562
+
563
+ def processing_finished(self):
564
+ """Handle completion of processing"""
565
+ self.log_text.append("-" * 80)
566
+ self.log_text.append("Processing complete!")
567
+ self.progress_bar.setValue(100)
568
+ self.run_btn.setEnabled(True)
569
+ self.cancel_btn.setEnabled(False)
570
+ self.status_bar.showMessage("Processing complete")
571
+
572
+ # Save settings
573
+ self.save_settings()
574
+
575
+ # Show completion message
576
+ QMessageBox.information(self, "Processing Complete", "Comment removal process has finished successfully.")
577
+
578
+ def handle_error(self, error_message):
579
+ """Display error messages"""
580
+ self.log_text.append(f"ERROR: {error_message}")
581
+ self.log_text.append("-" * 80)
582
+ self.log_text.append("Processing stopped due to errors")
583
+ self.progress_bar.setValue(0)
584
+ self.run_btn.setEnabled(True)
585
+ self.cancel_btn.setEnabled(False)
586
+ self.status_bar.showMessage("Processing stopped due to errors")
587
+
588
+ # Show error message
589
+ QMessageBox.critical(self, "Processing Error", error_message)
590
+
591
+ def print_help():
592
+ """Print detailed help information for CLI usage"""
593
+ help_text = """
594
+ Python Comment Remover - CLI Usage
595
+
596
+ This tool removes comments from Python files while preserving code structure.
597
+
598
+ Basic Usage:
599
+ python comment_remover.py [OPTIONS] INPUT
600
+
601
+ Required Arguments:
602
+ INPUT Path to input Python file or directory
603
+
604
+ Options:
605
+ -o, --output PATH Output file or directory (default: add suffix to filename)
606
+ -r, --recursive Process directory recursively (only for directory input)
607
+ -d, --remove-docstrings
608
+ Also remove docstrings
609
+ --suffix SUFFIX Suffix for output files (default: '_no_comments')
610
+ -h, --help Show this help message and exit
611
+
612
+ Examples:
613
+ 1. Process a single file:
614
+ python comment_remover.py input.py -o output.py
615
+
616
+ 2. Process a directory recursively, removing docstrings:
617
+ python comment_remover.py my_project/ -o cleaned_project/ -r -d
618
+
619
+ 3. Process all files in current directory with custom suffix:
620
+ python comment_remover.py . --suffix "_clean"
621
+ """
622
+ print(help_text)
623
+
624
+ def main_cli():
625
+ """Command line interface entry point"""
626
+ parser = argparse.ArgumentParser(
627
+ description="Remove comments from Python files while preserving indentation",
628
+ add_help=False # We'll handle help manually to show extended help
629
+ )
630
+ parser.add_argument('input', nargs='?', help='Input Python file or directory')
631
+ parser.add_argument('-o', '--output', help='Output file (if not specified, prints to stdout)')
632
+ parser.add_argument('-r', '--recursive', action='store_true', help='Process directory recursively')
633
+ parser.add_argument('-d', '--remove-docstrings', action='store_true', help='Also remove docstrings')
634
+ parser.add_argument('--suffix', default='_no_comments', help='Suffix for output files when processing directories')
635
+ parser.add_argument('-h', '--help', action='store_true', help='Show extended help information')
636
+
637
+ args = parser.parse_args()
638
+
639
+ if args.help or not args.input:
640
+ print_help()
641
+ if not args.input:
642
+ sys.exit(1)
643
+ else:
644
+ sys.exit(0)
645
+
646
+ input_path = Path(args.input)
647
+
648
+ if input_path.is_file():
649
+ # Process single file
650
+ if args.output:
651
+ output_path = Path(args.output)
652
+ # If output is a directory, use input filename
653
+ if output_path.is_dir():
654
+ output_path = output_path / input_path.name
655
+ else:
656
+ # Add suffix to filename in same directory
657
+ output_path = input_path.parent / f"{input_path.stem}{args.suffix}{input_path.suffix}"
658
+
659
+ process_file(input_path, output_path, args.remove_docstrings)
660
+
661
+ elif input_path.is_dir():
662
+ # Process directory
663
+ pattern = '**/*.py' if args.recursive else '*.py'
664
+
665
+ for py_file in input_path.glob(pattern):
666
+ if args.output:
667
+ # Create output directory structure
668
+ rel_path = py_file.relative_to(input_path)
669
+ output_dir = Path(args.output)
670
+ output_file = output_dir / rel_path
671
+ output_file.parent.mkdir(parents=True, exist_ok=True)
672
+ else:
673
+ # Add suffix to filename in same directory
674
+ output_file = py_file.parent / f"{py_file.stem}{args.suffix}{py_file.suffix}"
675
+
676
+ process_file(py_file, output_file, args.remove_docstrings)
677
+ else:
678
+ print(f"Error: {input_path} is not a valid file or directory", file=sys.stderr)
679
+ sys.exit(1)
680
+
681
+ def process_file(input_path, output_path=None, remove_docstrings=False):
682
+ """
683
+ Process a single Python file to remove comments.
684
+ """
685
+ try:
686
+ with open(input_path, 'r', encoding='utf-8') as f:
687
+ content = f.read()
688
+
689
+ processed_content = remove_comments_from_text(content, remove_docstrings)
690
+
691
+ if output_path:
692
+ # Ensure output directory exists
693
+ output_path.parent.mkdir(parents=True, exist_ok=True)
694
+ with open(output_path, 'w', encoding='utf-8') as f:
695
+ f.write(processed_content)
696
+ print(f"Processed: {input_path} -> {output_path}")
697
+ else:
698
+ # Output to stdout
699
+ print(processed_content)
700
+
701
+ except Exception as e:
702
+ print(f"Error processing {input_path}: {e}", file=sys.stderr)
703
+
704
+ if __name__ == "__main__":
705
+ # Check if we should run CLI or GUI
706
+ if len(sys.argv) > 1:
707
+ main_cli()
708
+ else:
709
+ app = QApplication(sys.argv)
710
+ window = PythonCommentRemoverGUI()
711
+ window.show()
712
+ sys.exit(app.exec())
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: crpy-tools
3
+ Version: 0.1.0
4
+ Summary: A CLI and GUI tool built with PySide6
5
+ Author-email: Youness <bouness@live.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Youness
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/bouness/crpy_tools
29
+ Project-URL: Repository, https://github.com/bouness/crpy_tools
30
+ Requires-Python: >=3.8
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: PySide6
34
+ Dynamic: license-file
35
+
36
+ # crpy-tools - Comment Removal Tools for Python
37
+
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ crpy-tools is a powerful tool for removing comments from Python files while preserving code structure and indentation. It offers both command-line interface (CLI) and graphical user interface (GUI) options for maximum flexibility.
41
+
42
+ ## Features
43
+
44
+ - 🚀 Remove all types of Python comments (#-style)
45
+ - 📝 Optionally remove docstrings
46
+ - 📁 Process single files or entire directories
47
+ - 🔁 Recursive directory processing
48
+ - 🎨 Clean code formatting with preserved indentation
49
+ - 💻 CLI for scripting and automation
50
+ - 🖥️ GUI for easy interactive use
51
+ - 📊 Progress reporting and logging
52
+
53
+ ## Installation
54
+
55
+ ### Requirements
56
+ - Python 3.7+
57
+ - PySide6 (for GUI)
58
+
59
+ ### Install from PyPI (coming soon)
60
+ ```bash
61
+ pip install crpy-tools
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ crpy_tools/__init__.py
5
+ crpy_tools/__main__.py
6
+ crpy_tools.egg-info/PKG-INFO
7
+ crpy_tools.egg-info/SOURCES.txt
8
+ crpy_tools.egg-info/dependency_links.txt
9
+ crpy_tools.egg-info/entry_points.txt
10
+ crpy_tools.egg-info/requires.txt
11
+ crpy_tools.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ crpy = crpy.__main__:main
@@ -0,0 +1 @@
1
+ PySide6
@@ -0,0 +1 @@
1
+ crpy_tools
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "crpy-tools"
7
+ version = "0.1.0"
8
+ description = "A CLI and GUI tool built with PySide6"
9
+ readme = "README.md"
10
+ license = {file = "LICENSE"}
11
+ authors = [
12
+ {name = "Youness", email = "bouness@live.com"}
13
+ ]
14
+ dependencies = [
15
+ "PySide6"
16
+ ]
17
+ requires-python = ">=3.8"
18
+
19
+ [project.scripts]
20
+ crpy = "crpy.__main__:main" # Update to match your CLI entry point
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/bouness/crpy_tools"
24
+ Repository = "https://github.com/bouness/crpy_tools"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+