skylos 1.2.2__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skylos might be problematic. Click here for more details.

Files changed (39) hide show
  1. {skylos-1.2.2 → skylos-2.0.0}/PKG-INFO +1 -1
  2. {skylos-1.2.2 → skylos-2.0.0}/README.md +34 -0
  3. {skylos-1.2.2 → skylos-2.0.0}/pyproject.toml +1 -1
  4. {skylos-1.2.2 → skylos-2.0.0}/setup.py +5 -2
  5. {skylos-1.2.2 → skylos-2.0.0}/skylos/__init__.py +1 -1
  6. {skylos-1.2.2 → skylos-2.0.0}/skylos/analyzer.py +58 -95
  7. {skylos-1.2.2 → skylos-2.0.0}/skylos/cli.py +46 -36
  8. {skylos-1.2.2 → skylos-2.0.0}/skylos/constants.py +25 -10
  9. {skylos-1.2.2 → skylos-2.0.0}/skylos/framework_aware.py +8 -2
  10. skylos-2.0.0/skylos/server.py +560 -0
  11. {skylos-1.2.2 → skylos-2.0.0}/skylos/test_aware.py +0 -1
  12. {skylos-1.2.2 → skylos-2.0.0}/skylos/visitor.py +70 -56
  13. {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/PKG-INFO +1 -1
  14. {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/SOURCES.txt +1 -0
  15. {skylos-1.2.2 → skylos-2.0.0}/setup.cfg +0 -0
  16. {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/dependency_links.txt +0 -0
  17. {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/entry_points.txt +0 -0
  18. {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/requires.txt +0 -0
  19. {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/top_level.txt +0 -0
  20. {skylos-1.2.2 → skylos-2.0.0}/test/__init__.py +0 -0
  21. {skylos-1.2.2 → skylos-2.0.0}/test/compare_tools.py +0 -0
  22. {skylos-1.2.2 → skylos-2.0.0}/test/conftest.py +0 -0
  23. {skylos-1.2.2 → skylos-2.0.0}/test/diagnostics.py +0 -0
  24. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/__init__.py +0 -0
  25. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/app.py +0 -0
  26. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/__init__.py +0 -0
  27. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/commands.py +0 -0
  28. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/models.py +0 -0
  29. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/routes.py +0 -0
  30. {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/utils.py +0 -0
  31. {skylos-1.2.2 → skylos-2.0.0}/test/test_analyzer.py +0 -0
  32. {skylos-1.2.2 → skylos-2.0.0}/test/test_changes_analyzer.py +0 -0
  33. {skylos-1.2.2 → skylos-2.0.0}/test/test_cli.py +0 -0
  34. {skylos-1.2.2 → skylos-2.0.0}/test/test_constants.py +0 -0
  35. {skylos-1.2.2 → skylos-2.0.0}/test/test_framework_aware.py +0 -0
  36. {skylos-1.2.2 → skylos-2.0.0}/test/test_integration.py +0 -0
  37. {skylos-1.2.2 → skylos-2.0.0}/test/test_skylos.py +0 -0
  38. {skylos-1.2.2 → skylos-2.0.0}/test/test_test_aware.py +0 -0
  39. {skylos-1.2.2 → skylos-2.0.0}/test/test_visitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 1.2.2
3
+ Version: 2.0.0
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -11,15 +11,23 @@
11
11
 
12
12
  > A static analysis tool for Python codebases written in Python (formerly was written in Rust but we ditched that) that detects unreachable functions and unused imports, aka dead code. Faster and better results than many alternatives like Flake8 and Pylint, and finding more dead code than Vulture in our tests with comparable speed.
13
13
 
14
+ <div align="center">
15
+ <img src="assets/FE_SS.png" alt="FE" width="800">
16
+ </div>
17
+
18
+
14
19
  ## Table of Contents
15
20
 
16
21
  - [Features](#features)
17
22
  - [Benchmark](#benchmark-you-can-find-this-benchmark-test-in-test-folder)
18
23
  - [Installation](#installation)
19
24
  - [Quick Start](#quick-start)
25
+ - [Web Interface](#web-interface)
20
26
  - [Understanding Confidence Levels](#understanding-confidence-levels)
21
27
  - [Test File Detection](#test-file-detection)
22
28
  - [Folder Management](#folder-management)
29
+ - [Ignoring Pragmas](#ignoring-pragmas)
30
+ - [Including & Excluding Files](#including--excluding-files)
23
31
  - [CLI Options](#cli-options)
24
32
  - [Example Output](#example-output)
25
33
  - [Interactive Mode](#interactive-mode)
@@ -88,6 +96,9 @@ pip install .
88
96
  # Analyze a project
89
97
  skylos /path/to/your/project
90
98
 
99
+ # To launch the front end
100
+ skylos run
101
+
91
102
  # Interactive mode - select items to remove
92
103
  skylos --interactive /path/to/your/project
93
104
 
@@ -101,6 +112,17 @@ skylos --json /path/to/your/project
101
112
  skylos path/to/your/file --confidence 20 ## or whatever value u wanna set
102
113
  ```
103
114
 
115
+ ## Web Interface
116
+
117
+ Skylos includes a modern web dashboard for interactive analysis:
118
+
119
+ ```bash
120
+ # Start web interface
121
+ skylos run
122
+
123
+ # Opens browser at http://localhost:5090
124
+ ```
125
+
104
126
  ## Understanding Confidence Levels
105
127
 
106
128
  Skylos uses a confidence-based system to try to handle Python's dynamic nature and web frameworks.
@@ -163,6 +185,18 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
163
185
  /project/test_data.py
164
186
  ```
165
187
 
188
+ ## Ignoring Pragmas
189
+
190
+ To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
191
+
192
+ Example
193
+
194
+ ```python
195
+ def with_logging(self, enabled: bool = True) -> "WebPath": # pragma: no skylos
196
+ new_path = WebPath(self._url)
197
+ return new_path
198
+ ```
199
+
166
200
  ## Including & Excluding Files
167
201
 
168
202
  ### Default Exclusions
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skylos"
7
- version = "1.2.2"
7
+ version = "2.0.0"
8
8
  requires-python = ">=3.9"
9
9
  description = "A static analysis tool for Python codebases"
10
10
  authors = [{name = "oha", email = "aaronoh2015@gmail.com"}]
@@ -2,10 +2,13 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="skylos",
5
- version="1.2.2",
5
+ version="2.0.0",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.9",
8
- install_requires=["inquirer>=3.0.0"],
8
+ install_requires=[
9
+ "inquirer>=3.0.0",
10
+ "flask>=2.0.0",
11
+ "flask-cors>=3.0.0"],
9
12
  classifiers=[
10
13
  "Development Status :: 4 - Beta",
11
14
  "Intended Audience :: Developers",
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "1.0.22"
3
+ __version__ = "2.0.0"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
@@ -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"):p[-1]=p[-1][:-3]
62
- if p[-1]=="__init__":p.pop()
63
- return".".join(p)
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, d in self.defs.items():
115
- if d.in_init and not d.simple_name.startswith('_'):
116
- d.is_exported = True
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 d in self.defs.values():
141
- simple_name_lookup[d.simple_name].append(d)
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 d in matches:
155
- d.references += 1
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
- c = 100
176
-
158
+ confidence=100
177
159
  if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
178
- c -= PENALTIES["private_name"]
160
+ confidence -= PENALTIES["private_name"]
179
161
  if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
180
- c -= PENALTIES["dunder_or_magic"]
162
+ confidence -= PENALTIES["dunder_or_magic"]
181
163
  if def_obj.type == "variable" and def_obj.simple_name == "_":
182
- c -= PENALTIES["underscored_var"]
164
+ confidence -= PENALTIES["underscored_var"]
183
165
  if def_obj.in_init and def_obj.type in ("function", "class"):
184
- c -= PENALTIES["in_init_file"]
166
+ confidence -= PENALTIES["in_init_file"]
185
167
  if def_obj.name.split(".")[0] in self.dynamic:
186
- c -= PENALTIES["dynamic_module"]
168
+ confidence -= PENALTIES["dynamic_module"]
187
169
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
188
- c -= PENALTIES["test_related"]
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
- c = min(c, framework_confidence)
174
+ confidence = min(confidence, framework_confidence)
193
175
 
194
176
  if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
195
- c = 0
177
+ confidence = 0
196
178
 
197
179
  if def_obj.type == "parameter":
198
180
  if def_obj.simple_name in ("self", "cls"):
199
- c = 0
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
- c = 0
185
+ confidence = 0
204
186
 
205
187
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
206
- c = 0
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
- c = 0
193
+ confidence = 0
212
194
 
213
- def_obj.confidence = max(c, 0)
195
+ def_obj.confidence = max(confidence, 0)
214
196
 
215
197
  def _apply_heuristics(self):
216
198
  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]
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(d)
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 m in methods:
226
- if m.simple_name in AUTO_CALLED: # __init__, __enter__, __exit__
227
- m.references += 1
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
- 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
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 d in self.defs.values():
286
- # skip anything on line that carries ignore pragma
287
- if d.line in self.ignored_lines:
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, ignored
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, set()
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];c=int(sys.argv[2])if len(sys.argv)>2 else 60
356
- result = analyze(p,c)
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🔍 Python Static Analysis Results")
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" Unreachable functions: {len(data['unused_functions'])}")
329
+ print(f" * Unreachable functions: {len(data['unused_functions'])}")
367
330
  if data["unused_imports"]:
368
- print(f" Unused imports: {len(data['unused_imports'])}")
331
+ print(f" * Unused imports: {len(data['unused_imports'])}")
369
332
  if data["unused_classes"]:
370
- print(f" Unused classes: {len(data['unused_classes'])}")
333
+ print(f" * Unused classes: {len(data['unused_classes'])}")
371
334
  if data["unused_variables"]:
372
- print(f" Unused variables: {len(data['unused_variables'])}")
335
+ print(f" * Unused variables: {len(data['unused_variables'])}")
373
336
 
374
337
  if data["unused_functions"]:
375
- print("\n📦 Unreachable Functions")
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📥 Unused Imports")
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📋 Unused Classes")
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📊 Unused Variables")
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(" Use --interactive to select specific items to remove")
410
- print(" Use --dry-run to preview changes before applying them")
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]")