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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "1.0.22"
3
+ __version__ = "2.1.0"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
skylos/analyzer.py CHANGED
@@ -6,61 +6,29 @@ import logging
6
6
  from pathlib import Path
7
7
  from collections import defaultdict
8
8
  from skylos.visitor import Visitor
9
- from skylos.constants import ( DEFAULT_EXCLUDE_FOLDERS, PENALTIES, AUTO_CALLED )
9
+ from skylos.constants import ( PENALTIES, AUTO_CALLED )
10
10
  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')
21
17
 
22
- def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folders=None):
23
- exclude_set = set()
24
-
25
- if use_defaults:
26
- exclude_set.update(DEFAULT_EXCLUDE_FOLDERS)
27
-
28
- if user_exclude_folders:
29
- exclude_set.update(user_exclude_folders)
30
-
31
- if include_folders:
32
- for folder in include_folders:
33
- exclude_set.discard(folder)
34
-
35
- return exclude_set
36
-
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
18
  class Skylos:
52
19
  def __init__(self):
53
20
  self.defs={}
54
21
  self.refs=[]
55
22
  self.dynamic=set()
56
23
  self.exports=defaultdict(set)
57
- self.ignored_lines:set[int]=set()
58
24
 
59
25
  def _module(self,root,f):
60
- p=list(f.relative_to(root).parts)
61
- if p[-1].endswith(".py"):p[-1]=p[-1][:-3]
62
- if p[-1]=="__init__":p.pop()
63
- return".".join(p)
26
+ p = list(f.relative_to(root).parts)
27
+ if p[-1].endswith(".py"):
28
+ p[-1] = p[-1][:-3]
29
+ if p[-1] == "__init__":
30
+ p.pop()
31
+ return ".".join(p)
64
32
 
65
33
  def _should_exclude_file(self, file_path, root_path, exclude_folders):
66
34
  if not exclude_folders:
@@ -111,9 +79,9 @@ class Skylos:
111
79
  return all_files, root
112
80
 
113
81
  def _mark_exports(self):
114
- for name, d in self.defs.items():
115
- if d.in_init and not d.simple_name.startswith('_'):
116
- d.is_exported = True
82
+ for name, definition in self.defs.items():
83
+ if definition.in_init and not definition.simple_name.startswith('_'):
84
+ definition.is_exported = True
117
85
 
118
86
  for mod, export_names in self.exports.items():
119
87
  for name in export_names:
@@ -137,23 +105,37 @@ class Skylos:
137
105
  break
138
106
 
139
107
  simple_name_lookup = defaultdict(list)
140
- for d in self.defs.values():
141
- simple_name_lookup[d.simple_name].append(d)
108
+ for definition in self.defs.values():
109
+ simple_name_lookup[definition.simple_name].append(definition)
142
110
 
143
111
  for ref, _ in self.refs:
144
112
  if ref in self.defs:
145
113
  self.defs[ref].references += 1
146
-
147
114
  if ref in import_to_original:
148
115
  original = import_to_original[ref]
149
116
  self.defs[original].references += 1
150
117
  continue
151
-
118
+
152
119
  simple = ref.split('.')[-1]
153
- matches = simple_name_lookup.get(simple, [])
154
- for d in matches:
155
- d.references += 1
156
-
120
+ ref_mod = ref.rsplit(".", 1)[0]
121
+ candidates = simple_name_lookup.get(simple, [])
122
+
123
+ if ref_mod:
124
+ filtered = []
125
+ for d in candidates:
126
+ if d.name.startswith(ref_mod + ".") and d.type != "import":
127
+ filtered.append(d)
128
+ candidates = filtered
129
+ else:
130
+ filtered = []
131
+ for d in candidates:
132
+ if d.type != "import":
133
+ filtered.append(d)
134
+ candidates = filtered
135
+
136
+ if len(candidates) == 1:
137
+ candidates[0].references += 1
138
+
157
139
  for module_name in self.dynamic:
158
140
  for def_name, def_obj in self.defs.items():
159
141
  if def_obj.name.startswith(f"{module_name}."):
@@ -172,59 +154,66 @@ class Skylos:
172
154
  return []
173
155
 
174
156
  def _apply_penalties(self, def_obj, visitor, framework):
175
- c = 100
176
-
157
+ confidence=100
177
158
  if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
178
- c -= PENALTIES["private_name"]
159
+ confidence -= PENALTIES["private_name"]
179
160
  if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
180
- c -= PENALTIES["dunder_or_magic"]
181
- if def_obj.type == "variable" and def_obj.simple_name == "_":
182
- c -= PENALTIES["underscored_var"]
161
+ confidence -= PENALTIES["dunder_or_magic"]
162
+ if def_obj.type == "variable" and def_obj.simple_name.isupper():
163
+ confidence = 0
183
164
  if def_obj.in_init and def_obj.type in ("function", "class"):
184
- c -= PENALTIES["in_init_file"]
165
+ confidence -= PENALTIES["in_init_file"]
185
166
  if def_obj.name.split(".")[0] in self.dynamic:
186
- c -= PENALTIES["dynamic_module"]
167
+ confidence -= PENALTIES["dynamic_module"]
187
168
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
188
- c -= PENALTIES["test_related"]
169
+ confidence -= PENALTIES["test_related"]
189
170
 
190
171
  framework_confidence = detect_framework_usage(def_obj, visitor=framework)
191
172
  if framework_confidence is not None:
192
- c = min(c, framework_confidence)
173
+ confidence = min(confidence, framework_confidence)
193
174
 
194
175
  if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
195
- c = 0
176
+ confidence = 0
196
177
 
197
178
  if def_obj.type == "parameter":
198
179
  if def_obj.simple_name in ("self", "cls"):
199
- c = 0
180
+ confidence = 0
200
181
  elif "." in def_obj.name:
201
182
  method_name = def_obj.name.split(".")[-2]
202
183
  if method_name.startswith("__") and method_name.endswith("__"):
203
- c = 0
184
+ confidence = 0
204
185
 
205
186
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
206
- c = 0
187
+ confidence = 0
207
188
 
208
189
  if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
209
190
  def_obj.simple_name in ("annotations", "absolute_import", "division",
210
191
  "print_function", "unicode_literals", "generator_stop")):
211
- c = 0
192
+ confidence = 0
212
193
 
213
- def_obj.confidence = max(c, 0)
194
+ def_obj.confidence = max(confidence, 0)
214
195
 
215
196
  def _apply_heuristics(self):
216
197
  class_methods = defaultdict(list)
217
- for d in self.defs.values():
218
- if d.type in ("method", "function") and "." in d.name:
219
- cls = d.name.rsplit(".", 1)[0]
198
+ for definition in self.defs.values():
199
+ if definition.type in ("method", "function") and "." in definition.name:
200
+ cls = definition.name.rsplit(".", 1)[0]
220
201
  if cls in self.defs and self.defs[cls].type == "class":
221
- class_methods[cls].append(d)
202
+ class_methods[cls].append(definition)
222
203
 
223
204
  for cls, methods in class_methods.items():
224
205
  if self.defs[cls].references > 0:
225
- for m in methods:
226
- if m.simple_name in AUTO_CALLED: # __init__, __enter__, __exit__
227
- m.references += 1
206
+ for method in methods:
207
+ if method.simple_name in AUTO_CALLED:
208
+ method.references += 1
209
+
210
+ if (method.simple_name.startswith("visit_") or
211
+ method.simple_name.startswith("leave_") or
212
+ method.simple_name.startswith("transform_")):
213
+ method.references += 1
214
+
215
+ if method.simple_name == "format" and cls.endswith("Formatter"):
216
+ method.references += 1
228
217
 
229
218
  def analyze(self, path, thr=60, exclude_folders=None):
230
219
  files, root = self._get_python_files(path, exclude_folders)
@@ -251,25 +240,11 @@ class Skylos:
251
240
 
252
241
  for file in files:
253
242
  mod = modmap[file]
243
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
254
244
 
255
- result = proc_file(file, mod)
256
-
257
- if len(result) == 7: ##new
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
245
+ for definition in defs:
246
+ self._apply_penalties(definition, test_flags, framework_flags)
247
+ self.defs[definition.name] = definition
273
248
 
274
249
  self.refs.extend(refs)
275
250
  self.dynamic.update(dyn)
@@ -278,18 +253,22 @@ class Skylos:
278
253
  self._mark_refs()
279
254
  self._apply_heuristics()
280
255
  self._mark_exports()
281
-
282
- thr = max(0, thr)
283
256
 
284
- unused = []
285
- for d in self.defs.values():
286
- # skip anything on line that carries ignore pragma
287
- if d.line in self.ignored_lines:
288
- continue
257
+ shown = 0
258
+
259
+ def def_sort_key(d):
260
+ return (d.type, d.name)
289
261
 
290
- if (d.references == 0 and not d.is_exported
291
- and d.confidence >= thr):
292
- unused.append(d.to_dict())
262
+ for d in sorted(self.defs.values(), key=def_sort_key):
263
+ if shown >= 50:
264
+ 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}")
266
+ shown += 1
267
+
268
+ unused = []
269
+ for definition in self.defs.values():
270
+ if definition.references == 0 and not definition.is_exported and definition.confidence > 0 and definition.confidence >= thr:
271
+ unused.append(definition.to_dict())
293
272
 
294
273
  result = {
295
274
  "unused_functions": [],
@@ -299,7 +278,7 @@ class Skylos:
299
278
  "unused_parameters": [],
300
279
  "analysis_summary": {
301
280
  "total_files": len(files),
302
- "excluded_folders": exclude_folders if exclude_folders else [],
281
+ "excluded_folders": exclude_folders or [],
303
282
  }
304
283
  }
305
284
 
@@ -325,7 +304,6 @@ def proc_file(file_or_args, mod=None):
325
304
 
326
305
  try:
327
306
  source = Path(file).read_text(encoding="utf-8")
328
- ignored = _collect_ignored_lines(source)
329
307
  tree = ast.parse(source)
330
308
 
331
309
  tv = TestAwareVisitor(filename=file)
@@ -333,11 +311,11 @@ def proc_file(file_or_args, mod=None):
333
311
 
334
312
  fv = FrameworkAwareVisitor(filename=file)
335
313
  fv.visit(tree)
336
-
314
+ fv.finalize()
337
315
  v = Visitor(mod, file)
338
316
  v.visit(tree)
339
317
 
340
- return v.defs, v.refs, v.dyn, v.exports, tv, fv, ignored
318
+ return v.defs, v.refs, v.dyn, v.exports, tv, fv
341
319
  except Exception as e:
342
320
  logger.error(f"{file}: {e}")
343
321
  if os.getenv("SKYLOS_DEBUG"):
@@ -345,55 +323,59 @@ def proc_file(file_or_args, mod=None):
345
323
  dummy_visitor = TestAwareVisitor(filename=file)
346
324
  dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
347
325
 
348
- return [], [], set(), set(), dummy_visitor, dummy_framework_visitor, set()
326
+ return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
349
327
 
350
328
  def analyze(path,conf=60, exclude_folders=None):
351
329
  return Skylos().analyze(path,conf, exclude_folders)
352
330
 
353
- if __name__=="__main__":
331
+ if __name__ == "__main__":
354
332
  if len(sys.argv)>1:
355
- p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
356
- result = analyze(p,c)
333
+ p = sys.argv[1]
334
+ confidence = int(sys.argv[2]) if len(sys.argv) >2 else 60
335
+ result = analyze(p,confidence)
357
336
 
358
337
  data = json.loads(result)
359
- print("\n🔍 Python Static Analysis Results")
338
+ print("\n Python Static Analysis Results")
360
339
  print("===================================\n")
361
340
 
362
- total_items = sum(len(items) for items in data.values())
341
+ total_items = 0
342
+ for key, items in data.items():
343
+ if key.startswith("unused_") and isinstance(items, list):
344
+ total_items += len(items)
363
345
 
364
346
  print("Summary:")
365
347
  if data["unused_functions"]:
366
- print(f" Unreachable functions: {len(data['unused_functions'])}")
348
+ print(f" * Unreachable functions: {len(data['unused_functions'])}")
367
349
  if data["unused_imports"]:
368
- print(f" Unused imports: {len(data['unused_imports'])}")
350
+ print(f" * Unused imports: {len(data['unused_imports'])}")
369
351
  if data["unused_classes"]:
370
- print(f" Unused classes: {len(data['unused_classes'])}")
352
+ print(f" * Unused classes: {len(data['unused_classes'])}")
371
353
  if data["unused_variables"]:
372
- print(f" Unused variables: {len(data['unused_variables'])}")
354
+ print(f" * Unused variables: {len(data['unused_variables'])}")
373
355
 
374
356
  if data["unused_functions"]:
375
- print("\n📦 Unreachable Functions")
357
+ print("\n - Unreachable Functions")
376
358
  print("=======================")
377
359
  for i, func in enumerate(data["unused_functions"], 1):
378
360
  print(f" {i}. {func['name']}")
379
361
  print(f" └─ {func['file']}:{func['line']}")
380
362
 
381
363
  if data["unused_imports"]:
382
- print("\n📥 Unused Imports")
364
+ print("\n - Unused Imports")
383
365
  print("================")
384
366
  for i, imp in enumerate(data["unused_imports"], 1):
385
367
  print(f" {i}. {imp['simple_name']}")
386
368
  print(f" └─ {imp['file']}:{imp['line']}")
387
369
 
388
370
  if data["unused_classes"]:
389
- print("\n📋 Unused Classes")
371
+ print("\n - Unused Classes")
390
372
  print("=================")
391
373
  for i, cls in enumerate(data["unused_classes"], 1):
392
374
  print(f" {i}. {cls['name']}")
393
375
  print(f" └─ {cls['file']}:{cls['line']}")
394
376
 
395
377
  if data["unused_variables"]:
396
- print("\n📊 Unused Variables")
378
+ print("\n - Unused Variables")
397
379
  print("==================")
398
380
  for i, var in enumerate(data["unused_variables"], 1):
399
381
  print(f" {i}. {var['name']}")
@@ -406,7 +388,7 @@ if __name__=="__main__":
406
388
  print(f"```")
407
389
 
408
390
  print("\nNext steps:")
409
- print(" Use --interactive to select specific items to remove")
410
- print(" Use --dry-run to preview changes before applying them")
391
+ print(" * Use --interactive to select specific items to remove")
392
+ print(" * Use --dry-run to preview changes before applying them")
411
393
  else:
412
394
  print("Usage: python Skylos.py <path> [confidence_threshold]")