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.
- crpy_tools-0.1.0/LICENSE +21 -0
- crpy_tools-0.1.0/PKG-INFO +61 -0
- crpy_tools-0.1.0/README.md +26 -0
- crpy_tools-0.1.0/crpy_tools/__init__.py +0 -0
- crpy_tools-0.1.0/crpy_tools/__main__.py +712 -0
- crpy_tools-0.1.0/crpy_tools.egg-info/PKG-INFO +61 -0
- crpy_tools-0.1.0/crpy_tools.egg-info/SOURCES.txt +11 -0
- crpy_tools-0.1.0/crpy_tools.egg-info/dependency_links.txt +1 -0
- crpy_tools-0.1.0/crpy_tools.egg-info/entry_points.txt +2 -0
- crpy_tools-0.1.0/crpy_tools.egg-info/requires.txt +1 -0
- crpy_tools-0.1.0/crpy_tools.egg-info/top_level.txt +1 -0
- crpy_tools-0.1.0/pyproject.toml +24 -0
- crpy_tools-0.1.0/setup.cfg +4 -0
crpy_tools-0.1.0/LICENSE
ADDED
@@ -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
|
+
[](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
|
+
[](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
|
+
[](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 @@
|
|
1
|
+
|
@@ -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"
|