skylos 1.2.2__py3-none-any.whl → 2.0.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.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +58 -95
- skylos/cli.py +46 -36
- skylos/constants.py +25 -10
- skylos/framework_aware.py +8 -2
- skylos/server.py +560 -0
- skylos/test_aware.py +0 -1
- skylos/visitor.py +70 -56
- {skylos-1.2.2.dist-info → skylos-2.0.0.dist-info}/METADATA +1 -1
- {skylos-1.2.2.dist-info → skylos-2.0.0.dist-info}/RECORD +13 -12
- {skylos-1.2.2.dist-info → skylos-2.0.0.dist-info}/WHEEL +0 -0
- {skylos-1.2.2.dist-info → skylos-2.0.0.dist-info}/entry_points.txt +0 -0
- {skylos-1.2.2.dist-info → skylos-2.0.0.dist-info}/top_level.txt +0 -0
skylos/__init__.py
CHANGED
skylos/analyzer.py
CHANGED
|
@@ -11,10 +11,6 @@ from skylos.test_aware import TestAwareVisitor
|
|
|
11
11
|
import os
|
|
12
12
|
import traceback
|
|
13
13
|
from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
|
-
import io
|
|
15
|
-
import tokenize
|
|
16
|
-
import re
|
|
17
|
-
import warnings
|
|
18
14
|
|
|
19
15
|
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
|
|
20
16
|
logger=logging.getLogger('Skylos')
|
|
@@ -34,33 +30,20 @@ def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folde
|
|
|
34
30
|
|
|
35
31
|
return exclude_set
|
|
36
32
|
|
|
37
|
-
IGNORE_PATTERNS = (
|
|
38
|
-
r"#\s*pragma:\s*no\s+skylos", ## our own pragma
|
|
39
|
-
r"#\s*pragma:\s*no\s+cover",
|
|
40
|
-
r"#\s*noqa(?:\b|:)", # flake8 style
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
def _collect_ignored_lines(source: str) -> set[int]:
|
|
44
|
-
ignores = set()
|
|
45
|
-
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
46
|
-
if tok.type == tokenize.COMMENT:
|
|
47
|
-
if any(re.search(pat, tok.string, flags=re.I) for pat in IGNORE_PATTERNS):
|
|
48
|
-
ignores.add(tok.start[0])
|
|
49
|
-
return ignores
|
|
50
|
-
|
|
51
33
|
class Skylos:
|
|
52
34
|
def __init__(self):
|
|
53
35
|
self.defs={}
|
|
54
36
|
self.refs=[]
|
|
55
37
|
self.dynamic=set()
|
|
56
38
|
self.exports=defaultdict(set)
|
|
57
|
-
self.ignored_lines:set[int]=set()
|
|
58
39
|
|
|
59
40
|
def _module(self,root,f):
|
|
60
|
-
p=list(f.relative_to(root).parts)
|
|
61
|
-
if p[-1].endswith(".py"):
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
p = list(f.relative_to(root).parts)
|
|
42
|
+
if p[-1].endswith(".py"):
|
|
43
|
+
p[-1] = p[-1][:-3]
|
|
44
|
+
if p[-1] == "__init__":
|
|
45
|
+
p.pop()
|
|
46
|
+
return ".".join(p)
|
|
64
47
|
|
|
65
48
|
def _should_exclude_file(self, file_path, root_path, exclude_folders):
|
|
66
49
|
if not exclude_folders:
|
|
@@ -111,9 +94,9 @@ class Skylos:
|
|
|
111
94
|
return all_files, root
|
|
112
95
|
|
|
113
96
|
def _mark_exports(self):
|
|
114
|
-
for name,
|
|
115
|
-
if
|
|
116
|
-
|
|
97
|
+
for name, definition in self.defs.items():
|
|
98
|
+
if definition.in_init and not definition.simple_name.startswith('_'):
|
|
99
|
+
definition.is_exported = True
|
|
117
100
|
|
|
118
101
|
for mod, export_names in self.exports.items():
|
|
119
102
|
for name in export_names:
|
|
@@ -137,8 +120,8 @@ class Skylos:
|
|
|
137
120
|
break
|
|
138
121
|
|
|
139
122
|
simple_name_lookup = defaultdict(list)
|
|
140
|
-
for
|
|
141
|
-
simple_name_lookup[
|
|
123
|
+
for definition in self.defs.values():
|
|
124
|
+
simple_name_lookup[definition.simple_name].append(definition)
|
|
142
125
|
|
|
143
126
|
for ref, _ in self.refs:
|
|
144
127
|
if ref in self.defs:
|
|
@@ -151,8 +134,8 @@ class Skylos:
|
|
|
151
134
|
|
|
152
135
|
simple = ref.split('.')[-1]
|
|
153
136
|
matches = simple_name_lookup.get(simple, [])
|
|
154
|
-
for
|
|
155
|
-
|
|
137
|
+
for definition in matches:
|
|
138
|
+
definition.references += 1
|
|
156
139
|
|
|
157
140
|
for module_name in self.dynamic:
|
|
158
141
|
for def_name, def_obj in self.defs.items():
|
|
@@ -172,59 +155,58 @@ class Skylos:
|
|
|
172
155
|
return []
|
|
173
156
|
|
|
174
157
|
def _apply_penalties(self, def_obj, visitor, framework):
|
|
175
|
-
|
|
176
|
-
|
|
158
|
+
confidence=100
|
|
177
159
|
if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
|
|
178
|
-
|
|
160
|
+
confidence -= PENALTIES["private_name"]
|
|
179
161
|
if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
|
|
180
|
-
|
|
162
|
+
confidence -= PENALTIES["dunder_or_magic"]
|
|
181
163
|
if def_obj.type == "variable" and def_obj.simple_name == "_":
|
|
182
|
-
|
|
164
|
+
confidence -= PENALTIES["underscored_var"]
|
|
183
165
|
if def_obj.in_init and def_obj.type in ("function", "class"):
|
|
184
|
-
|
|
166
|
+
confidence -= PENALTIES["in_init_file"]
|
|
185
167
|
if def_obj.name.split(".")[0] in self.dynamic:
|
|
186
|
-
|
|
168
|
+
confidence -= PENALTIES["dynamic_module"]
|
|
187
169
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
188
|
-
|
|
170
|
+
confidence -= PENALTIES["test_related"]
|
|
189
171
|
|
|
190
172
|
framework_confidence = detect_framework_usage(def_obj, visitor=framework)
|
|
191
173
|
if framework_confidence is not None:
|
|
192
|
-
|
|
174
|
+
confidence = min(confidence, framework_confidence)
|
|
193
175
|
|
|
194
176
|
if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
|
|
195
|
-
|
|
177
|
+
confidence = 0
|
|
196
178
|
|
|
197
179
|
if def_obj.type == "parameter":
|
|
198
180
|
if def_obj.simple_name in ("self", "cls"):
|
|
199
|
-
|
|
181
|
+
confidence = 0
|
|
200
182
|
elif "." in def_obj.name:
|
|
201
183
|
method_name = def_obj.name.split(".")[-2]
|
|
202
184
|
if method_name.startswith("__") and method_name.endswith("__"):
|
|
203
|
-
|
|
185
|
+
confidence = 0
|
|
204
186
|
|
|
205
187
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
206
|
-
|
|
188
|
+
confidence = 0
|
|
207
189
|
|
|
208
190
|
if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
|
|
209
191
|
def_obj.simple_name in ("annotations", "absolute_import", "division",
|
|
210
192
|
"print_function", "unicode_literals", "generator_stop")):
|
|
211
|
-
|
|
193
|
+
confidence = 0
|
|
212
194
|
|
|
213
|
-
def_obj.confidence = max(
|
|
195
|
+
def_obj.confidence = max(confidence, 0)
|
|
214
196
|
|
|
215
197
|
def _apply_heuristics(self):
|
|
216
198
|
class_methods = defaultdict(list)
|
|
217
|
-
for
|
|
218
|
-
if
|
|
219
|
-
cls =
|
|
199
|
+
for definition in self.defs.values():
|
|
200
|
+
if definition.type in ("method", "function") and "." in definition.name:
|
|
201
|
+
cls = definition.name.rsplit(".", 1)[0]
|
|
220
202
|
if cls in self.defs and self.defs[cls].type == "class":
|
|
221
|
-
class_methods[cls].append(
|
|
203
|
+
class_methods[cls].append(definition)
|
|
222
204
|
|
|
223
205
|
for cls, methods in class_methods.items():
|
|
224
206
|
if self.defs[cls].references > 0:
|
|
225
|
-
for
|
|
226
|
-
if
|
|
227
|
-
|
|
207
|
+
for method in methods:
|
|
208
|
+
if method.simple_name in AUTO_CALLED:
|
|
209
|
+
method.references += 1
|
|
228
210
|
|
|
229
211
|
def analyze(self, path, thr=60, exclude_folders=None):
|
|
230
212
|
files, root = self._get_python_files(path, exclude_folders)
|
|
@@ -251,25 +233,11 @@ class Skylos:
|
|
|
251
233
|
|
|
252
234
|
for file in files:
|
|
253
235
|
mod = modmap[file]
|
|
236
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
254
237
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
defs, refs, dyn, exports, test_flags, framework_flags, ignored = result
|
|
259
|
-
self.ignored_lines.update(ignored)
|
|
260
|
-
else: ##legacy
|
|
261
|
-
warnings.warn(
|
|
262
|
-
"proc_file() now returns 7 values (added ignored_lines). "
|
|
263
|
-
"The 6-value form is deprecated and will disappear.",
|
|
264
|
-
DeprecationWarning,
|
|
265
|
-
stacklevel=2,
|
|
266
|
-
)
|
|
267
|
-
defs, refs, dyn, exports, test_flags, framework_flags = result
|
|
268
|
-
|
|
269
|
-
# apply penalties while we still have the file-specific flags
|
|
270
|
-
for d in defs:
|
|
271
|
-
self._apply_penalties(d, test_flags, framework_flags)
|
|
272
|
-
self.defs[d.name] = d
|
|
238
|
+
for definition in defs:
|
|
239
|
+
self._apply_penalties(definition, test_flags, framework_flags)
|
|
240
|
+
self.defs[definition.name] = definition
|
|
273
241
|
|
|
274
242
|
self.refs.extend(refs)
|
|
275
243
|
self.dynamic.update(dyn)
|
|
@@ -282,14 +250,9 @@ class Skylos:
|
|
|
282
250
|
thr = max(0, thr)
|
|
283
251
|
|
|
284
252
|
unused = []
|
|
285
|
-
for
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
continue
|
|
289
|
-
|
|
290
|
-
if (d.references == 0 and not d.is_exported
|
|
291
|
-
and d.confidence >= thr):
|
|
292
|
-
unused.append(d.to_dict())
|
|
253
|
+
for definition in self.defs.values():
|
|
254
|
+
if definition.references == 0 and not definition.is_exported and definition.confidence > 0 and definition.confidence >= thr:
|
|
255
|
+
unused.append(definition.to_dict())
|
|
293
256
|
|
|
294
257
|
result = {
|
|
295
258
|
"unused_functions": [],
|
|
@@ -325,7 +288,6 @@ def proc_file(file_or_args, mod=None):
|
|
|
325
288
|
|
|
326
289
|
try:
|
|
327
290
|
source = Path(file).read_text(encoding="utf-8")
|
|
328
|
-
ignored = _collect_ignored_lines(source)
|
|
329
291
|
tree = ast.parse(source)
|
|
330
292
|
|
|
331
293
|
tv = TestAwareVisitor(filename=file)
|
|
@@ -337,7 +299,7 @@ def proc_file(file_or_args, mod=None):
|
|
|
337
299
|
v = Visitor(mod, file)
|
|
338
300
|
v.visit(tree)
|
|
339
301
|
|
|
340
|
-
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
302
|
+
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
341
303
|
except Exception as e:
|
|
342
304
|
logger.error(f"{file}: {e}")
|
|
343
305
|
if os.getenv("SKYLOS_DEBUG"):
|
|
@@ -345,55 +307,56 @@ def proc_file(file_or_args, mod=None):
|
|
|
345
307
|
dummy_visitor = TestAwareVisitor(filename=file)
|
|
346
308
|
dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
|
|
347
309
|
|
|
348
|
-
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
310
|
+
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
349
311
|
|
|
350
312
|
def analyze(path,conf=60, exclude_folders=None):
|
|
351
313
|
return Skylos().analyze(path,conf, exclude_folders)
|
|
352
314
|
|
|
353
|
-
if __name__=="__main__":
|
|
315
|
+
if __name__ == "__main__":
|
|
354
316
|
if len(sys.argv)>1:
|
|
355
|
-
p=sys.argv[1]
|
|
356
|
-
|
|
317
|
+
p = sys.argv[1]
|
|
318
|
+
confidence = int(sys.argv[2]) if len(sys.argv) >2 else 60
|
|
319
|
+
result = analyze(p,confidence)
|
|
357
320
|
|
|
358
321
|
data = json.loads(result)
|
|
359
|
-
print("\n
|
|
322
|
+
print("\n Python Static Analysis Results")
|
|
360
323
|
print("===================================\n")
|
|
361
324
|
|
|
362
325
|
total_items = sum(len(items) for items in data.values())
|
|
363
326
|
|
|
364
327
|
print("Summary:")
|
|
365
328
|
if data["unused_functions"]:
|
|
366
|
-
print(f"
|
|
329
|
+
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
367
330
|
if data["unused_imports"]:
|
|
368
|
-
print(f"
|
|
331
|
+
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
369
332
|
if data["unused_classes"]:
|
|
370
|
-
print(f"
|
|
333
|
+
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
371
334
|
if data["unused_variables"]:
|
|
372
|
-
print(f"
|
|
335
|
+
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
373
336
|
|
|
374
337
|
if data["unused_functions"]:
|
|
375
|
-
print("\n
|
|
338
|
+
print("\n - Unreachable Functions")
|
|
376
339
|
print("=======================")
|
|
377
340
|
for i, func in enumerate(data["unused_functions"], 1):
|
|
378
341
|
print(f" {i}. {func['name']}")
|
|
379
342
|
print(f" └─ {func['file']}:{func['line']}")
|
|
380
343
|
|
|
381
344
|
if data["unused_imports"]:
|
|
382
|
-
print("\n
|
|
345
|
+
print("\n - Unused Imports")
|
|
383
346
|
print("================")
|
|
384
347
|
for i, imp in enumerate(data["unused_imports"], 1):
|
|
385
348
|
print(f" {i}. {imp['simple_name']}")
|
|
386
349
|
print(f" └─ {imp['file']}:{imp['line']}")
|
|
387
350
|
|
|
388
351
|
if data["unused_classes"]:
|
|
389
|
-
print("\n
|
|
352
|
+
print("\n - Unused Classes")
|
|
390
353
|
print("=================")
|
|
391
354
|
for i, cls in enumerate(data["unused_classes"], 1):
|
|
392
355
|
print(f" {i}. {cls['name']}")
|
|
393
356
|
print(f" └─ {cls['file']}:{cls['line']}")
|
|
394
357
|
|
|
395
358
|
if data["unused_variables"]:
|
|
396
|
-
print("\n
|
|
359
|
+
print("\n - Unused Variables")
|
|
397
360
|
print("==================")
|
|
398
361
|
for i, var in enumerate(data["unused_variables"], 1):
|
|
399
362
|
print(f" {i}. {var['name']}")
|
|
@@ -406,7 +369,7 @@ if __name__=="__main__":
|
|
|
406
369
|
print(f"```")
|
|
407
370
|
|
|
408
371
|
print("\nNext steps:")
|
|
409
|
-
print("
|
|
410
|
-
print("
|
|
372
|
+
print(" * Use --interactive to select specific items to remove")
|
|
373
|
+
print(" * Use --dry-run to preview changes before applying them")
|
|
411
374
|
else:
|
|
412
375
|
print("Usage: python Skylos.py <path> [confidence_threshold]")
|
skylos/cli.py
CHANGED
|
@@ -4,7 +4,8 @@ import sys
|
|
|
4
4
|
import logging
|
|
5
5
|
import ast
|
|
6
6
|
import skylos
|
|
7
|
-
from skylos.
|
|
7
|
+
from skylos.constants import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
|
|
8
|
+
from skylos.server import start_server
|
|
8
9
|
|
|
9
10
|
try:
|
|
10
11
|
import inquirer
|
|
@@ -19,14 +20,11 @@ class Colors:
|
|
|
19
20
|
BLUE = '\033[94m'
|
|
20
21
|
MAGENTA = '\033[95m'
|
|
21
22
|
CYAN = '\033[96m'
|
|
22
|
-
WHITE = '\033[97m'
|
|
23
23
|
BOLD = '\033[1m'
|
|
24
|
-
UNDERLINE = '\033[4m'
|
|
25
24
|
RESET = '\033[0m'
|
|
26
25
|
GRAY = '\033[90m'
|
|
27
26
|
|
|
28
27
|
class CleanFormatter(logging.Formatter):
|
|
29
|
-
"""Custom formatter that removes timestamps and log levels for clean output"""
|
|
30
28
|
def format(self, record):
|
|
31
29
|
return record.getMessage()
|
|
32
30
|
|
|
@@ -62,6 +60,7 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
|
|
|
62
60
|
|
|
63
61
|
if original_line.startswith(f'import {import_name}'):
|
|
64
62
|
lines[line_idx] = ''
|
|
63
|
+
|
|
65
64
|
elif original_line.startswith('import ') and f' {import_name}' in original_line:
|
|
66
65
|
parts = original_line.split(' ', 1)[1].split(',')
|
|
67
66
|
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
@@ -69,6 +68,7 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
|
|
|
69
68
|
lines[line_idx] = f'import {", ".join(new_parts)}\n'
|
|
70
69
|
else:
|
|
71
70
|
lines[line_idx] = ''
|
|
71
|
+
|
|
72
72
|
elif original_line.startswith('from ') and import_name in original_line:
|
|
73
73
|
if f'import {import_name}' in original_line and ',' not in original_line:
|
|
74
74
|
lines[line_idx] = ''
|
|
@@ -90,7 +90,6 @@ def remove_unused_import(file_path: str, import_name: str, line_number: int) ->
|
|
|
90
90
|
return False
|
|
91
91
|
|
|
92
92
|
def remove_unused_function(file_path: str, function_name: str, line_number: int) -> bool:
|
|
93
|
-
# remove the entire def from the source code
|
|
94
93
|
try:
|
|
95
94
|
with open(file_path, 'r') as f:
|
|
96
95
|
content = f.read()
|
|
@@ -130,8 +129,8 @@ def remove_unused_function(file_path: str, function_name: str, line_number: int)
|
|
|
130
129
|
return True
|
|
131
130
|
|
|
132
131
|
return False
|
|
133
|
-
except
|
|
134
|
-
logging.error(f"Failed to remove function {function_name}
|
|
132
|
+
except:
|
|
133
|
+
logging.error(f"Failed to remove function {function_name}")
|
|
135
134
|
return False
|
|
136
135
|
|
|
137
136
|
def interactive_selection(logger, unused_functions, unused_imports):
|
|
@@ -146,6 +145,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
146
145
|
logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove:{Colors.RESET}")
|
|
147
146
|
|
|
148
147
|
function_choices = []
|
|
148
|
+
|
|
149
149
|
for item in unused_functions:
|
|
150
150
|
choice_text = f"{item['name']} ({item['file']}:{item['line']})"
|
|
151
151
|
function_choices.append((choice_text, item))
|
|
@@ -165,6 +165,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
165
165
|
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove:{Colors.RESET}")
|
|
166
166
|
|
|
167
167
|
import_choices = []
|
|
168
|
+
|
|
168
169
|
for item in unused_imports:
|
|
169
170
|
choice_text = f"{item['name']} ({item['file']}:{item['line']})"
|
|
170
171
|
import_choices.append((choice_text, item))
|
|
@@ -183,11 +184,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
183
184
|
return selected_functions, selected_imports
|
|
184
185
|
|
|
185
186
|
def print_badge(dead_code_count: int, logger):
|
|
186
|
-
"""Print appropriate badge based on dead code count"""
|
|
187
187
|
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
188
188
|
|
|
189
189
|
if dead_code_count == 0:
|
|
190
|
-
logger.info(f"
|
|
190
|
+
logger.info(f" Your code is 100% dead code free! Add this badge to your README:")
|
|
191
191
|
logger.info("```markdown")
|
|
192
192
|
logger.info("")
|
|
193
193
|
logger.info("```")
|
|
@@ -198,25 +198,34 @@ def print_badge(dead_code_count: int, logger):
|
|
|
198
198
|
logger.info("```")
|
|
199
199
|
|
|
200
200
|
def main() -> None:
|
|
201
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'run':
|
|
202
|
+
try:
|
|
203
|
+
start_server()
|
|
204
|
+
return
|
|
205
|
+
except ImportError:
|
|
206
|
+
print(f"{Colors.RED}Error: Flask is required {Colors.RESET}")
|
|
207
|
+
print(f"{Colors.YELLOW}Install with: pip install flask flask-cors{Colors.RESET}")
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
|
|
201
210
|
parser = argparse.ArgumentParser(
|
|
202
211
|
description="Detect unreachable functions and unused imports in a Python project"
|
|
203
212
|
)
|
|
204
|
-
parser.add_argument("path", help="Path to the Python project
|
|
213
|
+
parser.add_argument("path", help="Path to the Python project")
|
|
205
214
|
parser.add_argument(
|
|
206
215
|
"--json",
|
|
207
216
|
action="store_true",
|
|
208
|
-
help="Output raw JSON
|
|
217
|
+
help="Output raw JSON",
|
|
209
218
|
)
|
|
210
219
|
parser.add_argument(
|
|
211
220
|
"--output",
|
|
212
221
|
"-o",
|
|
213
222
|
type=str,
|
|
214
|
-
help="Write output to file
|
|
223
|
+
help="Write output to file",
|
|
215
224
|
)
|
|
216
225
|
parser.add_argument(
|
|
217
226
|
"--verbose", "-v",
|
|
218
227
|
action="store_true",
|
|
219
|
-
help="Enable verbose
|
|
228
|
+
help="Enable verbose"
|
|
220
229
|
)
|
|
221
230
|
parser.add_argument(
|
|
222
231
|
"--confidence",
|
|
@@ -228,12 +237,12 @@ def main() -> None:
|
|
|
228
237
|
parser.add_argument(
|
|
229
238
|
"--interactive", "-i",
|
|
230
239
|
action="store_true",
|
|
231
|
-
help="
|
|
240
|
+
help="Select items to remove"
|
|
232
241
|
)
|
|
233
242
|
parser.add_argument(
|
|
234
243
|
"--dry-run",
|
|
235
244
|
action="store_true",
|
|
236
|
-
help="Show what would be removed
|
|
245
|
+
help="Show what would be removed"
|
|
237
246
|
)
|
|
238
247
|
|
|
239
248
|
parser.add_argument(
|
|
@@ -251,14 +260,14 @@ def main() -> None:
|
|
|
251
260
|
dest="include_folders",
|
|
252
261
|
help="Force include a folder that would otherwise be excluded "
|
|
253
262
|
"(overrides both default and custom exclusions). "
|
|
254
|
-
"Example: --include-folder venv
|
|
263
|
+
"Example: --include-folder venv"
|
|
255
264
|
)
|
|
256
265
|
|
|
257
266
|
parser.add_argument(
|
|
258
267
|
"--no-default-excludes",
|
|
259
268
|
action="store_true",
|
|
260
269
|
help="Don't exclude default folders (__pycache__, .git, venv, etc.). "
|
|
261
|
-
"Only exclude folders
|
|
270
|
+
"Only exclude folders with --exclude-folder."
|
|
262
271
|
)
|
|
263
272
|
|
|
264
273
|
parser.add_argument(
|
|
@@ -283,6 +292,7 @@ def main() -> None:
|
|
|
283
292
|
if args.verbose:
|
|
284
293
|
logger.setLevel(logging.DEBUG)
|
|
285
294
|
logger.debug(f"Analyzing path: {args.path}")
|
|
295
|
+
|
|
286
296
|
if args.exclude_folders:
|
|
287
297
|
logger.debug(f"Excluding folders: {args.exclude_folders}")
|
|
288
298
|
|
|
@@ -317,15 +327,15 @@ def main() -> None:
|
|
|
317
327
|
unused_variables = result.get("unused_variables", [])
|
|
318
328
|
unused_classes = result.get("unused_classes", [])
|
|
319
329
|
|
|
320
|
-
logger.info(f"{Colors.CYAN}{Colors.BOLD}
|
|
330
|
+
logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
|
|
321
331
|
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
322
332
|
|
|
323
333
|
logger.info(f"\n{Colors.BOLD}Summary:{Colors.RESET}")
|
|
324
|
-
logger.info(f"
|
|
325
|
-
logger.info(f"
|
|
326
|
-
logger.info(f"
|
|
327
|
-
logger.info(f"
|
|
328
|
-
logger.info(f"
|
|
334
|
+
logger.info(f" * Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
|
|
335
|
+
logger.info(f" * Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
|
|
336
|
+
logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
|
|
337
|
+
logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
|
|
338
|
+
logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
|
|
329
339
|
|
|
330
340
|
if args.interactive and (unused_functions or unused_imports):
|
|
331
341
|
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
@@ -358,16 +368,16 @@ def main() -> None:
|
|
|
358
368
|
for func in selected_functions:
|
|
359
369
|
success = remove_unused_function(func['file'], func['name'], func['line'])
|
|
360
370
|
if success:
|
|
361
|
-
logger.info(f" {Colors.GREEN}
|
|
371
|
+
logger.info(f" {Colors.GREEN} {Colors.RESET} Removed function: {func['name']}")
|
|
362
372
|
else:
|
|
363
|
-
logger.error(f" {Colors.RED}
|
|
373
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {func['name']}")
|
|
364
374
|
|
|
365
375
|
for imp in selected_imports:
|
|
366
376
|
success = remove_unused_import(imp['file'], imp['name'], imp['line'])
|
|
367
377
|
if success:
|
|
368
|
-
logger.info(f" {Colors.GREEN}
|
|
378
|
+
logger.info(f" {Colors.GREEN} {Colors.RESET} Removed import: {imp['name']}")
|
|
369
379
|
else:
|
|
370
|
-
logger.error(f" {Colors.RED}
|
|
380
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {imp['name']}")
|
|
371
381
|
|
|
372
382
|
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
373
383
|
else:
|
|
@@ -379,16 +389,16 @@ def main() -> None:
|
|
|
379
389
|
|
|
380
390
|
else:
|
|
381
391
|
if unused_functions:
|
|
382
|
-
logger.info(f"\n{Colors.RED}{Colors.BOLD}
|
|
392
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD} - Unreachable Functions{Colors.RESET}")
|
|
383
393
|
logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
|
|
384
394
|
for i, item in enumerate(unused_functions, 1):
|
|
385
395
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
|
|
386
396
|
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
387
397
|
else:
|
|
388
|
-
logger.info(f"\n{Colors.GREEN}
|
|
398
|
+
logger.info(f"\n{Colors.GREEN} All functions are reachable!{Colors.RESET}")
|
|
389
399
|
|
|
390
400
|
if unused_imports:
|
|
391
|
-
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}
|
|
401
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD} - Unused Imports{Colors.RESET}")
|
|
392
402
|
logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
|
|
393
403
|
for i, item in enumerate(unused_imports, 1):
|
|
394
404
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
|
|
@@ -397,7 +407,7 @@ def main() -> None:
|
|
|
397
407
|
logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
|
|
398
408
|
|
|
399
409
|
if unused_parameters:
|
|
400
|
-
logger.info(f"\n{Colors.BLUE}{Colors.BOLD}
|
|
410
|
+
logger.info(f"\n{Colors.BLUE}{Colors.BOLD} - Unused Parameters{Colors.RESET}")
|
|
401
411
|
logger.info(f"{Colors.BLUE}{'=' * 18}{Colors.RESET}")
|
|
402
412
|
for i, item in enumerate(unused_parameters, 1):
|
|
403
413
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.BLUE}{item['name']}{Colors.RESET}")
|
|
@@ -406,14 +416,14 @@ def main() -> None:
|
|
|
406
416
|
logger.info(f"\n{Colors.GREEN}✓ All parameters are being used!{Colors.RESET}")
|
|
407
417
|
|
|
408
418
|
if unused_variables:
|
|
409
|
-
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}
|
|
419
|
+
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Variables{Colors.RESET}")
|
|
410
420
|
logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
|
|
411
421
|
for i, item in enumerate(unused_variables, 1):
|
|
412
422
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
|
|
413
423
|
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
414
424
|
|
|
415
425
|
if unused_classes:
|
|
416
|
-
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}
|
|
426
|
+
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
|
|
417
427
|
logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
|
|
418
428
|
for i, item in enumerate(unused_classes, 1):
|
|
419
429
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
|
|
@@ -428,9 +438,9 @@ def main() -> None:
|
|
|
428
438
|
|
|
429
439
|
if unused_functions or unused_imports:
|
|
430
440
|
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
431
|
-
logger.info(f"
|
|
432
|
-
logger.info(f"
|
|
433
|
-
logger.info(f"
|
|
441
|
+
logger.info(f" * Use --select specific items to remove")
|
|
442
|
+
logger.info(f" * Use --dry-run to preview changes")
|
|
443
|
+
logger.info(f" * Use --exclude-folder to skip directories")
|
|
434
444
|
|
|
435
445
|
if __name__ == "__main__":
|
|
436
446
|
main()
|
skylos/constants.py
CHANGED
|
@@ -2,18 +2,18 @@ import re
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
PENALTIES = {
|
|
5
|
-
"private_name":
|
|
6
|
-
"dunder_or_magic":
|
|
7
|
-
"underscored_var":
|
|
8
|
-
"in_init_file":
|
|
9
|
-
"dynamic_module":
|
|
10
|
-
"test_related":
|
|
11
|
-
|
|
5
|
+
"private_name": 80,
|
|
6
|
+
"dunder_or_magic": 100,
|
|
7
|
+
"underscored_var": 100,
|
|
8
|
+
"in_init_file": 15,
|
|
9
|
+
"dynamic_module": 40,
|
|
10
|
+
"test_related": 100,
|
|
11
|
+
"framework_magic": 40,
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
TEST_FILE_RE
|
|
14
|
+
TEST_FILE_RE = re.compile(r"(?:^|[/\\])tests?[/\\]|_test\.py$", re.I)
|
|
15
15
|
TEST_IMPORT_RE = re.compile(r"^(pytest|unittest|nose|mock|responses)(\.|$)")
|
|
16
|
-
TEST_DECOR_RE
|
|
16
|
+
TEST_DECOR_RE = re.compile(r"""^(
|
|
17
17
|
pytest\.(fixture|mark) |
|
|
18
18
|
patch(\.|$) |
|
|
19
19
|
responses\.activate |
|
|
@@ -39,4 +39,19 @@ def is_test_path(p: Path | str) -> bool:
|
|
|
39
39
|
return bool(TEST_FILE_RE.search(str(p)))
|
|
40
40
|
|
|
41
41
|
def is_framework_path(p: Path | str) -> bool:
|
|
42
|
-
return bool(FRAMEWORK_FILE_RE.search(str(p)))
|
|
42
|
+
return bool(FRAMEWORK_FILE_RE.search(str(p)))
|
|
43
|
+
|
|
44
|
+
def parse_exclude_folders(user_exclude_folders= None, use_defaults= True, include_folders= None):
|
|
45
|
+
exclude_folders = set()
|
|
46
|
+
|
|
47
|
+
if use_defaults:
|
|
48
|
+
exclude_folders.update(DEFAULT_EXCLUDE_FOLDERS)
|
|
49
|
+
|
|
50
|
+
if user_exclude_folders:
|
|
51
|
+
exclude_folders.update(user_exclude_folders)
|
|
52
|
+
|
|
53
|
+
if include_folders:
|
|
54
|
+
for folder in include_folders:
|
|
55
|
+
exclude_folders.discard(folder)
|
|
56
|
+
|
|
57
|
+
return exclude_folders
|