skylos 1.2.2__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +103 -121
- skylos/cli.py +66 -109
- skylos/codemods.py +89 -0
- skylos/constants.py +25 -10
- skylos/framework_aware.py +290 -90
- skylos/server.py +560 -0
- skylos/test_aware.py +0 -1
- skylos/visitor.py +249 -90
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/METADATA +4 -1
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/RECORD +16 -13
- test/test_codemods.py +153 -0
- test/test_framework_aware.py +176 -242
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/WHEEL +0 -0
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/entry_points.txt +0 -0
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/top_level.txt +0 -0
skylos/cli.py
CHANGED
|
@@ -2,9 +2,14 @@ import argparse
|
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
4
|
import logging
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
from skylos.analyzer import
|
|
5
|
+
from skylos.constants import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
|
|
6
|
+
from skylos.server import start_server
|
|
7
|
+
from skylos.analyzer import analyze as run_analyze
|
|
8
|
+
from skylos.codemods import (
|
|
9
|
+
remove_unused_import_cst,
|
|
10
|
+
remove_unused_function_cst,
|
|
11
|
+
)
|
|
12
|
+
import pathlib
|
|
8
13
|
|
|
9
14
|
try:
|
|
10
15
|
import inquirer
|
|
@@ -19,14 +24,11 @@ class Colors:
|
|
|
19
24
|
BLUE = '\033[94m'
|
|
20
25
|
MAGENTA = '\033[95m'
|
|
21
26
|
CYAN = '\033[96m'
|
|
22
|
-
WHITE = '\033[97m'
|
|
23
27
|
BOLD = '\033[1m'
|
|
24
|
-
UNDERLINE = '\033[4m'
|
|
25
28
|
RESET = '\033[0m'
|
|
26
29
|
GRAY = '\033[90m'
|
|
27
30
|
|
|
28
31
|
class CleanFormatter(logging.Formatter):
|
|
29
|
-
"""Custom formatter that removes timestamps and log levels for clean output"""
|
|
30
32
|
def format(self, record):
|
|
31
33
|
return record.getMessage()
|
|
32
34
|
|
|
@@ -52,84 +54,28 @@ def setup_logger(output_file=None):
|
|
|
52
54
|
|
|
53
55
|
return logger
|
|
54
56
|
|
|
55
|
-
def remove_unused_import(file_path
|
|
57
|
+
def remove_unused_import(file_path, import_name, line_number):
|
|
58
|
+
path = pathlib.Path(file_path)
|
|
56
59
|
try:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if original_line.startswith(f'import {import_name}'):
|
|
64
|
-
lines[line_idx] = ''
|
|
65
|
-
elif original_line.startswith('import ') and f' {import_name}' in original_line:
|
|
66
|
-
parts = original_line.split(' ', 1)[1].split(',')
|
|
67
|
-
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
68
|
-
if new_parts:
|
|
69
|
-
lines[line_idx] = f'import {", ".join(new_parts)}\n'
|
|
70
|
-
else:
|
|
71
|
-
lines[line_idx] = ''
|
|
72
|
-
elif original_line.startswith('from ') and import_name in original_line:
|
|
73
|
-
if f'import {import_name}' in original_line and ',' not in original_line:
|
|
74
|
-
lines[line_idx] = ''
|
|
75
|
-
else:
|
|
76
|
-
parts = original_line.split('import ', 1)[1].split(',')
|
|
77
|
-
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
78
|
-
if new_parts:
|
|
79
|
-
prefix = original_line.split(' import ')[0]
|
|
80
|
-
lines[line_idx] = f'{prefix} import {", ".join(new_parts)}\n'
|
|
81
|
-
else:
|
|
82
|
-
lines[line_idx] = ''
|
|
83
|
-
|
|
84
|
-
with open(file_path, 'w') as f:
|
|
85
|
-
f.writelines(lines)
|
|
86
|
-
|
|
60
|
+
src = path.read_text(encoding="utf-8")
|
|
61
|
+
new_code, changed = remove_unused_import_cst(src, import_name, line_number)
|
|
62
|
+
if not changed:
|
|
63
|
+
return False
|
|
64
|
+
path.write_text(new_code, encoding="utf-8")
|
|
87
65
|
return True
|
|
88
66
|
except Exception as e:
|
|
89
67
|
logging.error(f"Failed to remove import {import_name} from {file_path}: {e}")
|
|
90
68
|
return False
|
|
91
69
|
|
|
92
|
-
def remove_unused_function(file_path
|
|
93
|
-
|
|
70
|
+
def remove_unused_function(file_path, function_name, line_number):
|
|
71
|
+
path = pathlib.Path(file_path)
|
|
94
72
|
try:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for node in ast.walk(tree):
|
|
102
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
103
|
-
if (node.name in function_name and
|
|
104
|
-
node.lineno == line_number):
|
|
105
|
-
|
|
106
|
-
start_line = node.lineno - 1
|
|
107
|
-
|
|
108
|
-
if node.decorator_list:
|
|
109
|
-
start_line = node.decorator_list[0].lineno - 1
|
|
110
|
-
|
|
111
|
-
end_line = len(lines)
|
|
112
|
-
base_indent = len(lines[start_line]) - len(lines[start_line].lstrip())
|
|
113
|
-
|
|
114
|
-
for i in range(node.end_lineno, len(lines)):
|
|
115
|
-
if lines[i].strip() == '':
|
|
116
|
-
continue
|
|
117
|
-
current_indent = len(lines[i]) - len(lines[i].lstrip())
|
|
118
|
-
if current_indent <= base_indent and lines[i].strip():
|
|
119
|
-
end_line = i
|
|
120
|
-
break
|
|
121
|
-
|
|
122
|
-
while end_line < len(lines) and lines[end_line].strip() == '':
|
|
123
|
-
end_line += 1
|
|
124
|
-
|
|
125
|
-
new_lines = lines[:start_line] + lines[end_line:]
|
|
126
|
-
|
|
127
|
-
with open(file_path, 'w') as f:
|
|
128
|
-
f.write('\n'.join(new_lines) + '\n')
|
|
129
|
-
|
|
130
|
-
return True
|
|
131
|
-
|
|
132
|
-
return False
|
|
73
|
+
src = path.read_text(encoding="utf-8")
|
|
74
|
+
new_code, changed = remove_unused_function_cst(src, function_name, line_number)
|
|
75
|
+
if not changed:
|
|
76
|
+
return False
|
|
77
|
+
path.write_text(new_code, encoding="utf-8")
|
|
78
|
+
return True
|
|
133
79
|
except Exception as e:
|
|
134
80
|
logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
|
|
135
81
|
return False
|
|
@@ -143,9 +89,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
143
89
|
selected_imports = []
|
|
144
90
|
|
|
145
91
|
if unused_functions:
|
|
146
|
-
logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove:{Colors.RESET}")
|
|
92
|
+
logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove (hit spacebar to select):{Colors.RESET}")
|
|
147
93
|
|
|
148
94
|
function_choices = []
|
|
95
|
+
|
|
149
96
|
for item in unused_functions:
|
|
150
97
|
choice_text = f"{item['name']} ({item['file']}:{item['line']})"
|
|
151
98
|
function_choices.append((choice_text, item))
|
|
@@ -162,9 +109,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
162
109
|
selected_functions = answers['functions']
|
|
163
110
|
|
|
164
111
|
if unused_imports:
|
|
165
|
-
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove:{Colors.RESET}")
|
|
112
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove (hit spacebar to select):{Colors.RESET}")
|
|
166
113
|
|
|
167
114
|
import_choices = []
|
|
115
|
+
|
|
168
116
|
for item in unused_imports:
|
|
169
117
|
choice_text = f"{item['name']} ({item['file']}:{item['line']})"
|
|
170
118
|
import_choices.append((choice_text, item))
|
|
@@ -183,11 +131,10 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
183
131
|
return selected_functions, selected_imports
|
|
184
132
|
|
|
185
133
|
def print_badge(dead_code_count: int, logger):
|
|
186
|
-
"""Print appropriate badge based on dead code count"""
|
|
187
134
|
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
188
135
|
|
|
189
136
|
if dead_code_count == 0:
|
|
190
|
-
logger.info(f"
|
|
137
|
+
logger.info(f" Your code is 100% dead code free! Add this badge to your README:")
|
|
191
138
|
logger.info("```markdown")
|
|
192
139
|
logger.info("")
|
|
193
140
|
logger.info("```")
|
|
@@ -198,25 +145,34 @@ def print_badge(dead_code_count: int, logger):
|
|
|
198
145
|
logger.info("```")
|
|
199
146
|
|
|
200
147
|
def main() -> None:
|
|
148
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'run':
|
|
149
|
+
try:
|
|
150
|
+
start_server()
|
|
151
|
+
return
|
|
152
|
+
except ImportError:
|
|
153
|
+
print(f"{Colors.RED}Error: Flask is required {Colors.RESET}")
|
|
154
|
+
print(f"{Colors.YELLOW}Install with: pip install flask flask-cors{Colors.RESET}")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
201
157
|
parser = argparse.ArgumentParser(
|
|
202
158
|
description="Detect unreachable functions and unused imports in a Python project"
|
|
203
159
|
)
|
|
204
|
-
parser.add_argument("path", help="Path to the Python project
|
|
160
|
+
parser.add_argument("path", help="Path to the Python project")
|
|
205
161
|
parser.add_argument(
|
|
206
162
|
"--json",
|
|
207
163
|
action="store_true",
|
|
208
|
-
help="Output raw JSON
|
|
164
|
+
help="Output raw JSON",
|
|
209
165
|
)
|
|
210
166
|
parser.add_argument(
|
|
211
167
|
"--output",
|
|
212
168
|
"-o",
|
|
213
169
|
type=str,
|
|
214
|
-
help="Write output to file
|
|
170
|
+
help="Write output to file",
|
|
215
171
|
)
|
|
216
172
|
parser.add_argument(
|
|
217
173
|
"--verbose", "-v",
|
|
218
174
|
action="store_true",
|
|
219
|
-
help="Enable verbose
|
|
175
|
+
help="Enable verbose"
|
|
220
176
|
)
|
|
221
177
|
parser.add_argument(
|
|
222
178
|
"--confidence",
|
|
@@ -228,12 +184,12 @@ def main() -> None:
|
|
|
228
184
|
parser.add_argument(
|
|
229
185
|
"--interactive", "-i",
|
|
230
186
|
action="store_true",
|
|
231
|
-
help="
|
|
187
|
+
help="Select items to remove"
|
|
232
188
|
)
|
|
233
189
|
parser.add_argument(
|
|
234
190
|
"--dry-run",
|
|
235
191
|
action="store_true",
|
|
236
|
-
help="Show what would be removed
|
|
192
|
+
help="Show what would be removed"
|
|
237
193
|
)
|
|
238
194
|
|
|
239
195
|
parser.add_argument(
|
|
@@ -251,14 +207,14 @@ def main() -> None:
|
|
|
251
207
|
dest="include_folders",
|
|
252
208
|
help="Force include a folder that would otherwise be excluded "
|
|
253
209
|
"(overrides both default and custom exclusions). "
|
|
254
|
-
"Example: --include-folder venv
|
|
210
|
+
"Example: --include-folder venv"
|
|
255
211
|
)
|
|
256
212
|
|
|
257
213
|
parser.add_argument(
|
|
258
214
|
"--no-default-excludes",
|
|
259
215
|
action="store_true",
|
|
260
216
|
help="Don't exclude default folders (__pycache__, .git, venv, etc.). "
|
|
261
|
-
"Only exclude folders
|
|
217
|
+
"Only exclude folders with --exclude-folder."
|
|
262
218
|
)
|
|
263
219
|
|
|
264
220
|
parser.add_argument(
|
|
@@ -283,6 +239,7 @@ def main() -> None:
|
|
|
283
239
|
if args.verbose:
|
|
284
240
|
logger.setLevel(logging.DEBUG)
|
|
285
241
|
logger.debug(f"Analyzing path: {args.path}")
|
|
242
|
+
|
|
286
243
|
if args.exclude_folders:
|
|
287
244
|
logger.debug(f"Excluding folders: {args.exclude_folders}")
|
|
288
245
|
|
|
@@ -300,7 +257,7 @@ def main() -> None:
|
|
|
300
257
|
logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
|
|
301
258
|
|
|
302
259
|
try:
|
|
303
|
-
result_json =
|
|
260
|
+
result_json = run_analyze(args.path, conf=args.confidence, exclude_folders=list(final_exclude_folders))
|
|
304
261
|
result = json.loads(result_json)
|
|
305
262
|
|
|
306
263
|
except Exception as e:
|
|
@@ -317,15 +274,15 @@ def main() -> None:
|
|
|
317
274
|
unused_variables = result.get("unused_variables", [])
|
|
318
275
|
unused_classes = result.get("unused_classes", [])
|
|
319
276
|
|
|
320
|
-
logger.info(f"{Colors.CYAN}{Colors.BOLD}
|
|
277
|
+
logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
|
|
321
278
|
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
322
279
|
|
|
323
280
|
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"
|
|
281
|
+
logger.info(f" * Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
|
|
282
|
+
logger.info(f" * Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
|
|
283
|
+
logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
|
|
284
|
+
logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
|
|
285
|
+
logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
|
|
329
286
|
|
|
330
287
|
if args.interactive and (unused_functions or unused_imports):
|
|
331
288
|
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
@@ -358,16 +315,16 @@ def main() -> None:
|
|
|
358
315
|
for func in selected_functions:
|
|
359
316
|
success = remove_unused_function(func['file'], func['name'], func['line'])
|
|
360
317
|
if success:
|
|
361
|
-
logger.info(f" {Colors.GREEN}
|
|
318
|
+
logger.info(f" {Colors.GREEN} {Colors.RESET} Removed function: {func['name']}")
|
|
362
319
|
else:
|
|
363
|
-
logger.error(f" {Colors.RED}
|
|
320
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {func['name']}")
|
|
364
321
|
|
|
365
322
|
for imp in selected_imports:
|
|
366
323
|
success = remove_unused_import(imp['file'], imp['name'], imp['line'])
|
|
367
324
|
if success:
|
|
368
|
-
logger.info(f" {Colors.GREEN}
|
|
325
|
+
logger.info(f" {Colors.GREEN} {Colors.RESET} Removed import: {imp['name']}")
|
|
369
326
|
else:
|
|
370
|
-
logger.error(f" {Colors.RED}
|
|
327
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {imp['name']}")
|
|
371
328
|
|
|
372
329
|
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
373
330
|
else:
|
|
@@ -379,16 +336,16 @@ def main() -> None:
|
|
|
379
336
|
|
|
380
337
|
else:
|
|
381
338
|
if unused_functions:
|
|
382
|
-
logger.info(f"\n{Colors.RED}{Colors.BOLD}
|
|
339
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD} - Unreachable Functions{Colors.RESET}")
|
|
383
340
|
logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
|
|
384
341
|
for i, item in enumerate(unused_functions, 1):
|
|
385
342
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
|
|
386
343
|
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
387
344
|
else:
|
|
388
|
-
logger.info(f"\n{Colors.GREEN}
|
|
345
|
+
logger.info(f"\n{Colors.GREEN} All functions are reachable!{Colors.RESET}")
|
|
389
346
|
|
|
390
347
|
if unused_imports:
|
|
391
|
-
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}
|
|
348
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD} - Unused Imports{Colors.RESET}")
|
|
392
349
|
logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
|
|
393
350
|
for i, item in enumerate(unused_imports, 1):
|
|
394
351
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
|
|
@@ -397,7 +354,7 @@ def main() -> None:
|
|
|
397
354
|
logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
|
|
398
355
|
|
|
399
356
|
if unused_parameters:
|
|
400
|
-
logger.info(f"\n{Colors.BLUE}{Colors.BOLD}
|
|
357
|
+
logger.info(f"\n{Colors.BLUE}{Colors.BOLD} - Unused Parameters{Colors.RESET}")
|
|
401
358
|
logger.info(f"{Colors.BLUE}{'=' * 18}{Colors.RESET}")
|
|
402
359
|
for i, item in enumerate(unused_parameters, 1):
|
|
403
360
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.BLUE}{item['name']}{Colors.RESET}")
|
|
@@ -406,14 +363,14 @@ def main() -> None:
|
|
|
406
363
|
logger.info(f"\n{Colors.GREEN}✓ All parameters are being used!{Colors.RESET}")
|
|
407
364
|
|
|
408
365
|
if unused_variables:
|
|
409
|
-
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}
|
|
366
|
+
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Variables{Colors.RESET}")
|
|
410
367
|
logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
|
|
411
368
|
for i, item in enumerate(unused_variables, 1):
|
|
412
369
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
|
|
413
370
|
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
414
371
|
|
|
415
372
|
if unused_classes:
|
|
416
|
-
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD}
|
|
373
|
+
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
|
|
417
374
|
logger.info(f"{Colors.YELLOW}{'=' * 18}{Colors.RESET}")
|
|
418
375
|
for i, item in enumerate(unused_classes, 1):
|
|
419
376
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
|
|
@@ -428,9 +385,9 @@ def main() -> None:
|
|
|
428
385
|
|
|
429
386
|
if unused_functions or unused_imports:
|
|
430
387
|
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
431
|
-
logger.info(f"
|
|
432
|
-
logger.info(f"
|
|
433
|
-
logger.info(f"
|
|
388
|
+
logger.info(f" * Use --select specific items to remove")
|
|
389
|
+
logger.info(f" * Use --dry-run to preview changes")
|
|
390
|
+
logger.info(f" * Use --exclude-folder to skip directories")
|
|
434
391
|
|
|
435
392
|
if __name__ == "__main__":
|
|
436
393
|
main()
|
skylos/codemods.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import libcst as cst
|
|
3
|
+
from libcst.metadata import PositionProvider
|
|
4
|
+
|
|
5
|
+
def _bound_name_for_import_alias(alias: cst.ImportAlias):
|
|
6
|
+
if alias.asname:
|
|
7
|
+
return alias.asname.name.value
|
|
8
|
+
node = alias.name
|
|
9
|
+
while isinstance(node, cst.Attribute):
|
|
10
|
+
node = node.value
|
|
11
|
+
return node.value
|
|
12
|
+
|
|
13
|
+
class _RemoveImportAtLine(cst.CSTTransformer):
|
|
14
|
+
METADATA_DEPENDENCIES = (PositionProvider,)
|
|
15
|
+
|
|
16
|
+
def __init__(self, target_name, target_line):
|
|
17
|
+
self.target_name = target_name
|
|
18
|
+
self.target_line = target_line
|
|
19
|
+
self.changed = False
|
|
20
|
+
|
|
21
|
+
def _is_target_line(self, node: cst.CSTNode):
|
|
22
|
+
pos = self.get_metadata(PositionProvider, node, None)
|
|
23
|
+
return bool(pos and pos.start.line == self.target_line)
|
|
24
|
+
|
|
25
|
+
def _filter_aliases(self, aliases):
|
|
26
|
+
kept = []
|
|
27
|
+
for alias in aliases:
|
|
28
|
+
bound = _bound_name_for_import_alias(alias)
|
|
29
|
+
if bound == self.target_name:
|
|
30
|
+
self.changed = True
|
|
31
|
+
continue
|
|
32
|
+
kept.append(alias)
|
|
33
|
+
return kept
|
|
34
|
+
|
|
35
|
+
def leave_Import(self, orig: cst.Import, updated: cst.Import):
|
|
36
|
+
if not self._is_target_line(orig):
|
|
37
|
+
return updated
|
|
38
|
+
kept = self._filter_aliases(updated.names)
|
|
39
|
+
if not kept:
|
|
40
|
+
return cst.RemoveFromParent()
|
|
41
|
+
return updated.with_changes(names=tuple(kept))
|
|
42
|
+
|
|
43
|
+
def leave_ImportFrom(self, orig: cst.ImportFrom, updated: cst.ImportFrom):
|
|
44
|
+
if not self._is_target_line(orig):
|
|
45
|
+
return updated
|
|
46
|
+
if isinstance(updated.names, cst.ImportStar):
|
|
47
|
+
return updated
|
|
48
|
+
kept = self._filter_aliases(list(updated.names))
|
|
49
|
+
if not kept:
|
|
50
|
+
return cst.RemoveFromParent()
|
|
51
|
+
|
|
52
|
+
return updated.with_changes(names=tuple(kept))
|
|
53
|
+
|
|
54
|
+
class _RemoveFunctionAtLine(cst.CSTTransformer):
|
|
55
|
+
METADATA_DEPENDENCIES = (PositionProvider,)
|
|
56
|
+
|
|
57
|
+
def __init__(self, func_name, target_line):
|
|
58
|
+
self.func_name = func_name
|
|
59
|
+
self.target_line = target_line
|
|
60
|
+
self.changed = False
|
|
61
|
+
|
|
62
|
+
def _is_target(self, node: cst.CSTNode):
|
|
63
|
+
pos = self.get_metadata(PositionProvider, node, None)
|
|
64
|
+
return bool(pos and pos.start.line == self.target_line)
|
|
65
|
+
|
|
66
|
+
def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
|
|
67
|
+
if self._is_target(orig) and (orig.name.value in self.func_name):
|
|
68
|
+
self.changed = True
|
|
69
|
+
return cst.RemoveFromParent()
|
|
70
|
+
return updated
|
|
71
|
+
|
|
72
|
+
def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
|
|
73
|
+
if self._is_target(orig) and (orig.name.value in self.func_name):
|
|
74
|
+
self.changed = True
|
|
75
|
+
return cst.RemoveFromParent()
|
|
76
|
+
|
|
77
|
+
return updated
|
|
78
|
+
|
|
79
|
+
def remove_unused_import_cst(code, import_name, line_number):
|
|
80
|
+
wrapper = cst.MetadataWrapper(cst.parse_module(code))
|
|
81
|
+
tx = _RemoveImportAtLine(import_name, line_number)
|
|
82
|
+
new_mod = wrapper.visit(tx)
|
|
83
|
+
return new_mod.code, tx.changed
|
|
84
|
+
|
|
85
|
+
def remove_unused_function_cst(code, func_name, line_number):
|
|
86
|
+
wrapper = cst.MetadataWrapper(cst.parse_module(code))
|
|
87
|
+
tx = _RemoveFunctionAtLine(func_name, line_number)
|
|
88
|
+
new_mod = wrapper.visit(tx)
|
|
89
|
+
return new_mod.code, tx.changed
|
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
|