skylos 2.1.1__py3-none-any.whl → 2.2.2__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 CHANGED
@@ -1,8 +1,10 @@
1
- from skylos.analyzer import analyze
1
+ __version__ = "2.2.2"
2
2
 
3
- __version__ = "2.1.1"
3
+ def analyze(*args, **kwargs):
4
+ from .analyzer import analyze as _analyze
5
+ return _analyze(*args, **kwargs)
4
6
 
5
7
  def debug_test():
6
8
  return "debug-ok"
7
9
 
8
- __all__ = ["analyze", "debug_test", "__version__"]
10
+ __all__ = ["analyze", "debug_test", "__version__"]
skylos/analyzer.py CHANGED
@@ -7,10 +7,11 @@ from pathlib import Path
7
7
  from collections import defaultdict
8
8
  from skylos.visitor import Visitor
9
9
  from skylos.constants import ( PENALTIES, AUTO_CALLED )
10
- from skylos.test_aware import TestAwareVisitor
10
+ from skylos.visitors.test_aware import TestAwareVisitor
11
+ from skylos.rules.secrets import scan_ctx as _secrets_scan_ctx
11
12
  import os
12
13
  import traceback
13
- from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
14
+ from skylos.visitors.framework_aware import FrameworkAwareVisitor, detect_framework_usage
14
15
 
15
16
  logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
16
17
  logger=logging.getLogger('Skylos')
@@ -155,18 +156,40 @@ class Skylos:
155
156
 
156
157
  def _apply_penalties(self, def_obj, visitor, framework):
157
158
  confidence=100
159
+
160
+ if getattr(visitor, "ignore_lines", None) and def_obj.line in visitor.ignore_lines:
161
+ def_obj.confidence = 0
162
+ return
163
+
164
+ if def_obj.type == "variable" and def_obj.simple_name == "_":
165
+ def_obj.confidence = 0
166
+ return
167
+
158
168
  if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
159
169
  confidence -= PENALTIES["private_name"]
170
+
160
171
  if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
161
172
  confidence -= PENALTIES["dunder_or_magic"]
162
- if def_obj.type == "variable" and def_obj.simple_name.isupper():
163
- confidence = 0
173
+
164
174
  if def_obj.in_init and def_obj.type in ("function", "class"):
165
175
  confidence -= PENALTIES["in_init_file"]
176
+
166
177
  if def_obj.name.split(".")[0] in self.dynamic:
167
178
  confidence -= PENALTIES["dynamic_module"]
179
+
168
180
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
169
181
  confidence -= PENALTIES["test_related"]
182
+
183
+ if def_obj.type == "variable" and getattr(framework, "dataclass_fields", None):
184
+ if def_obj.name in framework.dataclass_fields:
185
+ def_obj.confidence = 0
186
+ return
187
+
188
+ if def_obj.type == "variable":
189
+ fr = getattr(framework, "first_read_lineno", {}).get(def_obj.name)
190
+ if fr is not None and fr >= def_obj.line:
191
+ def_obj.confidence = 0
192
+ return
170
193
 
171
194
  framework_confidence = detect_framework_usage(def_obj, visitor=framework)
172
195
  if framework_confidence is not None:
@@ -215,7 +238,7 @@ class Skylos:
215
238
  if method.simple_name == "format" and cls.endswith("Formatter"):
216
239
  method.references += 1
217
240
 
218
- def analyze(self, path, thr=60, exclude_folders=None):
241
+ def analyze(self, path, thr=60, exclude_folders= None, enable_secrets = False):
219
242
  files, root = self._get_python_files(path, exclude_folders)
220
243
 
221
244
  if not files:
@@ -238,6 +261,7 @@ class Skylos:
238
261
  for f in files:
239
262
  modmap[f] = self._module(root, f)
240
263
 
264
+ all_secrets = []
241
265
  for file in files:
242
266
  mod = modmap[file]
243
267
  defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
@@ -250,6 +274,16 @@ class Skylos:
250
274
  self.dynamic.update(dyn)
251
275
  self.exports[mod].update(exports)
252
276
 
277
+ if enable_secrets and _secrets_scan_ctx is not None:
278
+ try:
279
+ src_lines = Path(file).read_text(encoding="utf-8", errors="ignore").splitlines(True)
280
+ ctx = {"relpath": str(file), "lines": src_lines, "tree": None}
281
+ findings = list(_secrets_scan_ctx(ctx))
282
+ if findings:
283
+ all_secrets.extend(findings)
284
+ except Exception:
285
+ pass
286
+
253
287
  self._mark_refs()
254
288
  self._apply_heuristics()
255
289
  self._mark_exports()
@@ -262,7 +296,7 @@ class Skylos:
262
296
  for d in sorted(self.defs.values(), key=def_sort_key):
263
297
  if shown >= 50:
264
298
  break
265
- print(f" {d.type:<8} refs={d.references:<2} conf={d.confidence:<3} exported={d.is_exported} line={d.line:<4} {d.name}")
299
+ print(f" type={d.type} refs={d.references} conf={d.confidence} exported={d.is_exported} line={d.line} name={d.name}")
266
300
  shown += 1
267
301
 
268
302
  unused = []
@@ -281,6 +315,9 @@ class Skylos:
281
315
  "excluded_folders": exclude_folders or [],
282
316
  }
283
317
  }
318
+
319
+ if enable_secrets and all_secrets:
320
+ result["secrets"] = all_secrets
284
321
 
285
322
  for u in unused:
286
323
  if u["type"] in ("function", "method"):
@@ -304,10 +341,13 @@ def proc_file(file_or_args, mod=None):
304
341
 
305
342
  try:
306
343
  source = Path(file).read_text(encoding="utf-8")
307
- tree = ast.parse(source)
344
+ ignore_lines = {i for i, line in enumerate(source.splitlines(), start=1)
345
+ if "pragma: no skylos" in line}
346
+ tree = ast.parse(source)
308
347
 
309
348
  tv = TestAwareVisitor(filename=file)
310
349
  tv.visit(tree)
350
+ tv.ignore_lines = ignore_lines
311
351
 
312
352
  fv = FrameworkAwareVisitor(filename=file)
313
353
  fv.visit(tree)
@@ -315,18 +355,23 @@ def proc_file(file_or_args, mod=None):
315
355
  v = Visitor(mod, file)
316
356
  v.visit(tree)
317
357
 
358
+ fv.dataclass_fields = getattr(v, "dataclass_fields", set())
359
+ fv.first_read_lineno = getattr(v, "first_read_lineno", {})
360
+
318
361
  return v.defs, v.refs, v.dyn, v.exports, tv, fv
362
+
319
363
  except Exception as e:
320
364
  logger.error(f"{file}: {e}")
321
365
  if os.getenv("SKYLOS_DEBUG"):
322
366
  logger.error(traceback.format_exc())
323
367
  dummy_visitor = TestAwareVisitor(filename=file)
368
+ dummy_visitor.ignore_lines = set()
324
369
  dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
325
370
 
326
371
  return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
327
372
 
328
- def analyze(path,conf=60, exclude_folders=None):
329
- return Skylos().analyze(path,conf, exclude_folders)
373
+ def analyze(path, conf=60, exclude_folders=None, enable_secrets=False):
374
+ return Skylos().analyze(path,conf, exclude_folders, enable_secrets)
330
375
 
331
376
  if __name__ == "__main__":
332
377
  if len(sys.argv)>1:
skylos/cli.py CHANGED
@@ -8,8 +8,11 @@ from skylos.analyzer import analyze as run_analyze
8
8
  from skylos.codemods import (
9
9
  remove_unused_import_cst,
10
10
  remove_unused_function_cst,
11
+ comment_out_unused_import_cst,
12
+ comment_out_unused_function_cst,
11
13
  )
12
14
  import pathlib
15
+ import skylos
13
16
 
14
17
  try:
15
18
  import inquirer
@@ -79,6 +82,32 @@ def remove_unused_function(file_path, function_name, line_number):
79
82
  except Exception as e:
80
83
  logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
81
84
  return False
85
+
86
+ def comment_out_unused_import(file_path, import_name, line_number, marker="SKYLOS DEADCODE"):
87
+ path = pathlib.Path(file_path)
88
+ try:
89
+ src = path.read_text(encoding="utf-8")
90
+ new_code, changed = comment_out_unused_import_cst(src, import_name, line_number, marker=marker)
91
+ if not changed:
92
+ return False
93
+ path.write_text(new_code, encoding="utf-8")
94
+ return True
95
+ except Exception as e:
96
+ logging.error(f"Failed to comment out import {import_name} from {file_path}: {e}")
97
+ return False
98
+
99
+ def comment_out_unused_function(file_path, function_name, line_number, marker="SKYLOS DEADCODE"):
100
+ path = pathlib.Path(file_path)
101
+ try:
102
+ src = path.read_text(encoding="utf-8")
103
+ new_code, changed = comment_out_unused_function_cst(src, function_name, line_number, marker=marker)
104
+ if not changed:
105
+ return False
106
+ path.write_text(new_code, encoding="utf-8")
107
+ return True
108
+ except Exception as e:
109
+ logging.error(f"Failed to comment out function {function_name} from {file_path}: {e}")
110
+ return False
82
111
 
83
112
  def interactive_selection(logger, unused_functions, unused_imports):
84
113
  if not INTERACTIVE_AVAILABLE:
@@ -109,7 +138,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
109
138
  selected_functions = answers['functions']
110
139
 
111
140
  if unused_imports:
112
- logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove (hit spacebar to select):{Colors.RESET}")
141
+ logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to act on (hit spacebar to select):{Colors.RESET}")
113
142
 
114
143
  import_choices = []
115
144
 
@@ -144,7 +173,7 @@ def print_badge(dead_code_count: int, logger):
144
173
  logger.info(f"![Dead Code: {dead_code_count}](https://img.shields.io/badge/Dead_Code-{dead_code_count}_detected-orange?logo=codacy&logoColor=red)")
145
174
  logger.info("```")
146
175
 
147
- def main() -> None:
176
+ def main():
148
177
  if len(sys.argv) > 1 and sys.argv[1] == 'run':
149
178
  try:
150
179
  start_server()
@@ -158,11 +187,26 @@ def main() -> None:
158
187
  description="Detect unreachable functions and unused imports in a Python project"
159
188
  )
160
189
  parser.add_argument("path", help="Path to the Python project")
190
+
191
+ parser.add_argument(
192
+ "--version",
193
+ action="version",
194
+ version=f"skylos {skylos.__version__}",
195
+ help="Show version and exit"
196
+ )
197
+
161
198
  parser.add_argument(
162
199
  "--json",
163
200
  action="store_true",
164
201
  help="Output raw JSON",
165
202
  )
203
+
204
+ parser.add_argument(
205
+ "--comment-out",
206
+ action="store_true",
207
+ help="Comment out selected dead code instead of deleting it",
208
+ )
209
+
166
210
  parser.add_argument(
167
211
  "--output",
168
212
  "-o",
@@ -223,12 +267,15 @@ def main() -> None:
223
267
  help="List the default excluded folders and exit."
224
268
  )
225
269
 
270
+ parser.add_argument("--secrets", action="store_true",
271
+ help="Scan for API keys. Off by default.")
272
+
226
273
  args = parser.parse_args()
227
274
 
228
275
  if args.list_default_excludes:
229
276
  print("Default excluded folders:")
230
277
  for folder in sorted(DEFAULT_EXCLUDE_FOLDERS):
231
- print(f" {folder}")
278
+ print(f" {folder}")
232
279
  print(f"\nTotal: {len(DEFAULT_EXCLUDE_FOLDERS)} folders")
233
280
  print("\nUse --no-default-excludes to disable these exclusions")
234
281
  print("Use --include-folder <folder> to force include specific folders")
@@ -257,7 +304,7 @@ def main() -> None:
257
304
  logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
258
305
 
259
306
  try:
260
- result_json = run_analyze(args.path, conf=args.confidence, exclude_folders=list(final_exclude_folders))
307
+ result_json = run_analyze(args.path, conf=args.confidence, enable_secrets=bool(args.secrets), exclude_folders=list(final_exclude_folders))
261
308
  result = json.loads(result_json)
262
309
 
263
310
  except Exception as e:
@@ -273,6 +320,7 @@ def main() -> None:
273
320
  unused_parameters = result.get("unused_parameters", [])
274
321
  unused_variables = result.get("unused_variables", [])
275
322
  unused_classes = result.get("unused_classes", [])
323
+ secrets_findings = result.get("secrets", [])
276
324
 
277
325
  logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
278
326
  logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
@@ -283,49 +331,75 @@ def main() -> None:
283
331
  logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
284
332
  logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
285
333
  logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
334
+ if secrets_findings:
335
+ logger.info(f" * Secrets: {Colors.RED}{len(secrets_findings)}{Colors.RESET}")
286
336
 
287
337
  if args.interactive and (unused_functions or unused_imports):
288
338
  logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
289
339
  selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
290
340
 
291
341
  if selected_functions or selected_imports:
292
- logger.info(f"\n{Colors.BOLD}Selected items to remove:{Colors.RESET}")
342
+ logger.info(f"\n{Colors.BOLD}Selected items to process:{Colors.RESET}")
293
343
 
294
344
  if selected_functions:
295
- logger.info(f" Functions: {len(selected_functions)}")
345
+ logger.info(f" Functions: {len(selected_functions)}")
296
346
  for func in selected_functions:
297
- logger.info(f" - {func['name']} ({func['file']}:{func['line']})")
347
+ logger.info(f" - {func['name']} ({func['file']}: {func['line']})")
298
348
 
299
349
  if selected_imports:
300
- logger.info(f" Imports: {len(selected_imports)}")
350
+ logger.info(f" Imports: {len(selected_imports)}")
301
351
  for imp in selected_imports:
302
- logger.info(f" - {imp['name']} ({imp['file']}:{imp['line']})")
352
+ logger.info(f" - {imp['name']} ({imp['file']}: {imp['line']})")
303
353
 
304
354
  if not args.dry_run:
355
+ if args.comment_out:
356
+ confirm_verb = "comment out"
357
+ else:
358
+ confirm_verb = "remove"
359
+
305
360
  questions = [
306
361
  inquirer.Confirm('confirm',
307
- message="Are you sure you want to remove these items?",
362
+ message="Are you sure you want to process these items?",
308
363
  default=False)
309
364
  ]
310
365
  answers = inquirer.prompt(questions)
311
366
 
312
367
  if answers and answers['confirm']:
313
- logger.info(f"\n{Colors.YELLOW}Removing selected items...{Colors.RESET}")
314
-
368
+ action = "Commenting out" if args.comment_out else "Removing"
369
+ logger.info(f"\n{Colors.YELLOW}{action} selected items...{Colors.RESET}")
370
+
371
+ action_func = comment_out_unused_function if args.comment_out else remove_unused_function
372
+ if args.comment_out:
373
+ action_past = "Commented out"
374
+ action_verb = "comment out"
375
+ else:
376
+ action_past = "Removed"
377
+ action_verb = "remove"
378
+
315
379
  for func in selected_functions:
316
- success = remove_unused_function(func['file'], func['name'], func['line'])
380
+ success = action_func(func['file'], func['name'], func['line'])
381
+
317
382
  if success:
318
- logger.info(f" {Colors.GREEN} {Colors.RESET} Removed function: {func['name']}")
383
+ logger.info(f" {Colors.GREEN} {Colors.RESET} {action_past} function: {func['name']}")
319
384
  else:
320
- logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {func['name']}")
321
-
385
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {func['name']}")
386
+
387
+ import_func = comment_out_unused_import if args.comment_out else remove_unused_import
388
+ if args.comment_out:
389
+ action_past = "Commented out"
390
+ action_verb = "comment out"
391
+ else:
392
+ action_past = "Removed"
393
+ action_verb = "remove"
394
+
322
395
  for imp in selected_imports:
323
- success = remove_unused_import(imp['file'], imp['name'], imp['line'])
396
+ success = import_func(imp['file'], imp['name'], imp['line'])
397
+
324
398
  if success:
325
- logger.info(f" {Colors.GREEN} {Colors.RESET} Removed import: {imp['name']}")
399
+ logger.info(f" {Colors.GREEN} {Colors.RESET} {action_past} import: {imp['name']}")
326
400
  else:
327
- logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {imp['name']}")
328
-
401
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {imp['name']}")
402
+
329
403
  logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
330
404
  else:
331
405
  logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
@@ -368,6 +442,8 @@ def main() -> None:
368
442
  for i, item in enumerate(unused_variables, 1):
369
443
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
370
444
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
445
+ else:
446
+ logger.info(f"\n{Colors.GREEN}✓ All variables are being used!{Colors.RESET}")
371
447
 
372
448
  if unused_classes:
373
449
  logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
@@ -375,9 +451,18 @@ def main() -> None:
375
451
  for i, item in enumerate(unused_classes, 1):
376
452
  logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
377
453
  logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
378
-
379
454
  else:
380
- logger.info(f"\n{Colors.GREEN}✓ All variables are being used!{Colors.RESET}")
455
+ logger.info(f"\n{Colors.GREEN}✓ All classes are being used!{Colors.RESET}")
456
+
457
+ if secrets_findings:
458
+ logger.info(f"\n{Colors.RED}{Colors.BOLD} - Secrets{Colors.RESET}")
459
+ logger.info(f"{Colors.RED}{'=' * 9}{Colors.RESET}")
460
+ for i, s in enumerate(secrets_findings[:20], 1):
461
+ provider = s.get("provider", "generic")
462
+ where = f"{s.get('file','?')}:{s.get('line','?')}"
463
+ prev = s.get("preview", "****")
464
+ msg = s.get("message", "Secret detected")
465
+ logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{msg} [{provider}] {Colors.GRAY}({where}){Colors.RESET} -> {prev}")
381
466
 
382
467
  dead_code_count = len(unused_functions) + len(unused_imports) + len(unused_variables) + len(unused_classes) + len(unused_parameters)
383
468
 
skylos/codemods.py CHANGED
@@ -2,6 +2,155 @@ from __future__ import annotations
2
2
  import libcst as cst
3
3
  from libcst.metadata import PositionProvider
4
4
 
5
+ class _CommentOutBlock(cst.CSTTransformer):
6
+
7
+ METADATA_DEPENDENCIES = (PositionProvider,)
8
+
9
+ def __init__(self, module_code, marker = "SKYLOS DEADCODE"):
10
+ self.module_code = module_code.splitlines(True)
11
+ self.marker = marker
12
+
13
+ def _comment_block(self, start_line, end_line):
14
+ lines = self.module_code[start_line - 1:end_line]
15
+ out = []
16
+ out.append(cst.EmptyLine(comment=cst.Comment(f"# {self.marker} START (lines {start_line}-{end_line})")))
17
+ for raw in lines:
18
+ out.append(cst.EmptyLine(comment=cst.Comment("# " + raw.rstrip("\n"))))
19
+ out.append(cst.EmptyLine(comment=cst.Comment(f"# {self.marker} END")))
20
+ return out
21
+
22
+ class _CommentOutFunctionAtLine(_CommentOutBlock):
23
+ def __init__(self, func_name, target_line, module_code, marker):
24
+ super().__init__(module_code, marker)
25
+ self.func_name = func_name
26
+ self.target_line = target_line
27
+ self.changed = False
28
+
29
+ def _is_target(self, node: cst.CSTNode):
30
+ pos = self.get_metadata(PositionProvider, node, None)
31
+ return pos and pos.start.line == self.target_line
32
+
33
+ def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
34
+ if self._is_target(orig) and (orig.name.value == self.func_name):
35
+ self.changed = True
36
+ pos = self.get_metadata(PositionProvider, orig)
37
+ return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
38
+ return updated
39
+
40
+ def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
41
+ if self._is_target(orig) and (orig.name.value == self.func_name):
42
+ self.changed = True
43
+ pos = self.get_metadata(PositionProvider, orig)
44
+ return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
45
+ return updated
46
+
47
+ class _CommentOutImportAtLine(_CommentOutBlock):
48
+
49
+ def __init__(self, target_name, target_line, module_code, marker):
50
+ super().__init__(module_code, marker)
51
+ self.target_name = target_name
52
+ self.target_line = target_line
53
+ self.changed = False
54
+
55
+ def _is_target_line(self, node: cst.CSTNode):
56
+ pos = self.get_metadata(PositionProvider, node, None)
57
+ return bool(pos and (pos.start.line <= self.target_line <= pos.end.line))
58
+
59
+ def _render_single_alias_text(self, head, alias: cst.ImportAlias, is_from):
60
+ if is_from:
61
+ alias_txt = alias.name.code
62
+ if alias.asname:
63
+ alias_txt += f" as {alias.asname.name.value}"
64
+ return f"from {head} import {alias_txt}"
65
+ else:
66
+ alias_txt = alias.name.code
67
+ if alias.asname:
68
+ alias_txt += f" as {alias.asname.name.value}"
69
+ return f"import {alias_txt}"
70
+
71
+ def _split_aliases(self, aliases, head, is_from):
72
+ kept = []
73
+ removed_for_comment= []
74
+ for alias in list(aliases):
75
+ bound = _bound_name_for_import_alias(alias)
76
+ if bound == self.target_name:
77
+ self.changed = True
78
+ removed_for_comment.append(self._render_single_alias_text(head, alias, is_from))
79
+ else:
80
+ kept.append(alias)
81
+ return kept, removed_for_comment
82
+
83
+ def leave_Import(self, orig: cst.Import, updated: cst.Import):
84
+ if not self._is_target_line(orig):
85
+ return updated
86
+
87
+ head = ""
88
+ kept, removed = self._split_aliases(updated.names, head, is_from=False)
89
+
90
+ if not removed:
91
+ return updated
92
+
93
+ pos = self.get_metadata(PositionProvider, orig)
94
+ if not kept:
95
+ return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
96
+
97
+ commented = []
98
+ for txt in removed:
99
+ comment = cst.Comment(f"# {self.marker}: {txt}")
100
+ commented.append(cst.EmptyLine(comment=comment))
101
+
102
+ kept_import = updated.with_changes(names=tuple(kept))
103
+ all_nodes = [kept_import] + commented
104
+ return cst.FlattenSentinel(all_nodes)
105
+
106
+ def leave_ImportFrom(self, orig: cst.ImportFrom, updated: cst.ImportFrom):
107
+ if not self._is_target_line(orig) or isinstance(updated.names, cst.ImportStar):
108
+ return updated
109
+
110
+ if updated.relative:
111
+ dots = "." * len(updated.relative)
112
+ else:
113
+ dots = ""
114
+
115
+ if updated.module is not None:
116
+ modname = updated.module.code
117
+ else:
118
+ modname = ""
119
+
120
+ mod = f"{dots}{modname}"
121
+
122
+ kept, removed = self._split_aliases(list(updated.names), mod, is_from=True)
123
+
124
+ if not removed:
125
+ return updated
126
+ pos = self.get_metadata(PositionProvider, orig)
127
+
128
+ if not kept:
129
+ comment_block = self._comment_block(pos.start.line, pos.end.line)
130
+ return cst.FlattenSentinel(comment_block)
131
+
132
+ commented = []
133
+ for txt in removed:
134
+ comment = cst.Comment(f"# {self.marker}: {txt}")
135
+ commented.append(cst.EmptyLine(comment=comment))
136
+
137
+ updated_import = updated.with_changes(names=tuple(kept))
138
+ all_nodes = [updated_import] + commented
139
+
140
+ return cst.FlattenSentinel(all_nodes)
141
+
142
+ def comment_out_unused_function_cst(code, func_name, line_number, marker = "SKYLOS DEADCODE"):
143
+ wrapper = cst.MetadataWrapper(cst.parse_module(code))
144
+ tx = _CommentOutFunctionAtLine(func_name, line_number, code, marker)
145
+ new_mod = wrapper.visit(tx)
146
+ return new_mod.code, tx.changed
147
+
148
+ def comment_out_unused_import_cst(code, import_name, line_number, marker = "SKYLOS DEADCODE"):
149
+ wrapper = cst.MetadataWrapper(cst.parse_module(code))
150
+ tx = _CommentOutImportAtLine(import_name, line_number, code, marker)
151
+ new_mod = wrapper.visit(tx)
152
+ return new_mod.code, tx.changed
153
+
5
154
  def _bound_name_for_import_alias(alias: cst.ImportAlias):
6
155
  if alias.asname:
7
156
  return alias.asname.name.value
@@ -20,8 +169,8 @@ class _RemoveImportAtLine(cst.CSTTransformer):
20
169
 
21
170
  def _is_target_line(self, node: cst.CSTNode):
22
171
  pos = self.get_metadata(PositionProvider, node, None)
23
- return bool(pos and pos.start.line == self.target_line)
24
-
172
+ return bool(pos and (pos.start.line <= self.target_line <= pos.end.line))
173
+
25
174
  def _filter_aliases(self, aliases):
26
175
  kept = []
27
176
  for alias in aliases:
@@ -61,16 +210,16 @@ class _RemoveFunctionAtLine(cst.CSTTransformer):
61
210
 
62
211
  def _is_target(self, node: cst.CSTNode):
63
212
  pos = self.get_metadata(PositionProvider, node, None)
64
- return bool(pos and pos.start.line == self.target_line)
213
+ return pos and pos.start.line == self.target_line
65
214
 
66
215
  def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
67
- if self._is_target(orig) and (orig.name.value in self.func_name):
216
+ if self._is_target(orig) and (orig.name.value == self.func_name):
68
217
  self.changed = True
69
218
  return cst.RemoveFromParent()
70
219
  return updated
71
220
 
72
221
  def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
73
- if self._is_target(orig) and (orig.name.value in self.func_name):
222
+ if self._is_target(orig) and (orig.name.value == self.func_name):
74
223
  self.changed = True
75
224
  return cst.RemoveFromParent()
76
225
 
skylos/constants.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import re
2
- from pathlib import Path
3
2
 
4
3
  PENALTIES = {
5
4
  "private_name": 80,
@@ -35,10 +34,10 @@ DEFAULT_EXCLUDE_FOLDERS = {
35
34
  "htmlcov", ".coverage", "build", "dist", "*.egg-info", "venv", ".venv"
36
35
  }
37
36
 
38
- def is_test_path(p: Path | str) -> bool:
37
+ def is_test_path(p):
39
38
  return bool(TEST_FILE_RE.search(str(p)))
40
39
 
41
- def is_framework_path(p: Path | str) -> bool:
40
+ def is_framework_path(p):
42
41
  return bool(FRAMEWORK_FILE_RE.search(str(p)))
43
42
 
44
43
  def parse_exclude_folders(user_exclude_folders= None, use_defaults= True, include_folders= None):
File without changes