skylos 1.2.1__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.1 → skylos-2.0.0}/PKG-INFO +1 -1
  2. {skylos-1.2.1 → skylos-2.0.0}/README.md +35 -0
  3. {skylos-1.2.1 → skylos-2.0.0}/pyproject.toml +1 -1
  4. {skylos-1.2.1 → skylos-2.0.0}/setup.py +5 -2
  5. {skylos-1.2.1 → skylos-2.0.0}/skylos/__init__.py +1 -1
  6. {skylos-1.2.1 → skylos-2.0.0}/skylos/analyzer.py +58 -92
  7. {skylos-1.2.1 → skylos-2.0.0}/skylos/cli.py +46 -36
  8. {skylos-1.2.1 → skylos-2.0.0}/skylos/constants.py +25 -10
  9. {skylos-1.2.1 → skylos-2.0.0}/skylos/framework_aware.py +8 -2
  10. skylos-2.0.0/skylos/server.py +560 -0
  11. {skylos-1.2.1 → skylos-2.0.0}/skylos/test_aware.py +0 -1
  12. {skylos-1.2.1 → skylos-2.0.0}/skylos/visitor.py +70 -56
  13. {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/PKG-INFO +1 -1
  14. {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/SOURCES.txt +1 -0
  15. {skylos-1.2.1 → skylos-2.0.0}/setup.cfg +0 -0
  16. {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/dependency_links.txt +0 -0
  17. {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/entry_points.txt +0 -0
  18. {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/requires.txt +0 -0
  19. {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/top_level.txt +0 -0
  20. {skylos-1.2.1 → skylos-2.0.0}/test/__init__.py +0 -0
  21. {skylos-1.2.1 → skylos-2.0.0}/test/compare_tools.py +0 -0
  22. {skylos-1.2.1 → skylos-2.0.0}/test/conftest.py +0 -0
  23. {skylos-1.2.1 → skylos-2.0.0}/test/diagnostics.py +0 -0
  24. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/__init__.py +0 -0
  25. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/app.py +0 -0
  26. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/__init__.py +0 -0
  27. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/commands.py +0 -0
  28. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/models.py +0 -0
  29. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/routes.py +0 -0
  30. {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/utils.py +0 -0
  31. {skylos-1.2.1 → skylos-2.0.0}/test/test_analyzer.py +0 -0
  32. {skylos-1.2.1 → skylos-2.0.0}/test/test_changes_analyzer.py +0 -0
  33. {skylos-1.2.1 → skylos-2.0.0}/test/test_cli.py +0 -0
  34. {skylos-1.2.1 → skylos-2.0.0}/test/test_constants.py +0 -0
  35. {skylos-1.2.1 → skylos-2.0.0}/test/test_framework_aware.py +0 -0
  36. {skylos-1.2.1 → skylos-2.0.0}/test/test_integration.py +0 -0
  37. {skylos-1.2.1 → skylos-2.0.0}/test/test_skylos.py +0 -0
  38. {skylos-1.2.1 → skylos-2.0.0}/test/test_test_aware.py +0 -0
  39. {skylos-1.2.1 → 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.1
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)
@@ -41,6 +49,7 @@
41
49
  * **Unused Classes**: Detects classes that are not instantiated or inherited
42
50
  * **Unused Imports**: Identifies imports that are not used
43
51
  * **Folder Management**: Inclusion/exclusion of directories
52
+ * **Ignore Pragmas**: Skip lines tagged with `# pragma: no skylos`, `# pragma: no cover`, or `# noqa`
44
53
 
45
54
  ## Benchmark (You can find this benchmark test in `test` folder)
46
55
 
@@ -87,6 +96,9 @@ pip install .
87
96
  # Analyze a project
88
97
  skylos /path/to/your/project
89
98
 
99
+ # To launch the front end
100
+ skylos run
101
+
90
102
  # Interactive mode - select items to remove
91
103
  skylos --interactive /path/to/your/project
92
104
 
@@ -100,6 +112,17 @@ skylos --json /path/to/your/project
100
112
  skylos path/to/your/file --confidence 20 ## or whatever value u wanna set
101
113
  ```
102
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
+
103
126
  ## Understanding Confidence Levels
104
127
 
105
128
  Skylos uses a confidence-based system to try to handle Python's dynamic nature and web frameworks.
@@ -162,6 +185,18 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
162
185
  /project/test_data.py
163
186
  ```
164
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
+
165
200
  ## Including & Excluding Files
166
201
 
167
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.1"
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.1",
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,11 +250,9 @@ class Skylos:
282
250
  thr = max(0, thr)
283
251
 
284
252
  unused = []
285
- for d in self.defs.values():
286
- if (d.references == 0 and not d.is_exported
287
- and d.confidence >= thr
288
- and d.line not in self.ignored_lines):
289
- 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())
290
256
 
291
257
  result = {
292
258
  "unused_functions": [],
@@ -322,7 +288,6 @@ def proc_file(file_or_args, mod=None):
322
288
 
323
289
  try:
324
290
  source = Path(file).read_text(encoding="utf-8")
325
- ignored = _collect_ignored_lines(source)
326
291
  tree = ast.parse(source)
327
292
 
328
293
  tv = TestAwareVisitor(filename=file)
@@ -334,7 +299,7 @@ def proc_file(file_or_args, mod=None):
334
299
  v = Visitor(mod, file)
335
300
  v.visit(tree)
336
301
 
337
- return v.defs, v.refs, v.dyn, v.exports, tv, fv, ignored
302
+ return v.defs, v.refs, v.dyn, v.exports, tv, fv
338
303
  except Exception as e:
339
304
  logger.error(f"{file}: {e}")
340
305
  if os.getenv("SKYLOS_DEBUG"):
@@ -342,55 +307,56 @@ def proc_file(file_or_args, mod=None):
342
307
  dummy_visitor = TestAwareVisitor(filename=file)
343
308
  dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
344
309
 
345
- return [], [], set(), set(), dummy_visitor, dummy_framework_visitor, set()
310
+ return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
346
311
 
347
312
  def analyze(path,conf=60, exclude_folders=None):
348
313
  return Skylos().analyze(path,conf, exclude_folders)
349
314
 
350
- if __name__=="__main__":
315
+ if __name__ == "__main__":
351
316
  if len(sys.argv)>1:
352
- p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
353
- 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)
354
320
 
355
321
  data = json.loads(result)
356
- print("\n🔍 Python Static Analysis Results")
322
+ print("\n Python Static Analysis Results")
357
323
  print("===================================\n")
358
324
 
359
325
  total_items = sum(len(items) for items in data.values())
360
326
 
361
327
  print("Summary:")
362
328
  if data["unused_functions"]:
363
- print(f" Unreachable functions: {len(data['unused_functions'])}")
329
+ print(f" * Unreachable functions: {len(data['unused_functions'])}")
364
330
  if data["unused_imports"]:
365
- print(f" Unused imports: {len(data['unused_imports'])}")
331
+ print(f" * Unused imports: {len(data['unused_imports'])}")
366
332
  if data["unused_classes"]:
367
- print(f" Unused classes: {len(data['unused_classes'])}")
333
+ print(f" * Unused classes: {len(data['unused_classes'])}")
368
334
  if data["unused_variables"]:
369
- print(f" Unused variables: {len(data['unused_variables'])}")
335
+ print(f" * Unused variables: {len(data['unused_variables'])}")
370
336
 
371
337
  if data["unused_functions"]:
372
- print("\n📦 Unreachable Functions")
338
+ print("\n - Unreachable Functions")
373
339
  print("=======================")
374
340
  for i, func in enumerate(data["unused_functions"], 1):
375
341
  print(f" {i}. {func['name']}")
376
342
  print(f" └─ {func['file']}:{func['line']}")
377
343
 
378
344
  if data["unused_imports"]:
379
- print("\n📥 Unused Imports")
345
+ print("\n - Unused Imports")
380
346
  print("================")
381
347
  for i, imp in enumerate(data["unused_imports"], 1):
382
348
  print(f" {i}. {imp['simple_name']}")
383
349
  print(f" └─ {imp['file']}:{imp['line']}")
384
350
 
385
351
  if data["unused_classes"]:
386
- print("\n📋 Unused Classes")
352
+ print("\n - Unused Classes")
387
353
  print("=================")
388
354
  for i, cls in enumerate(data["unused_classes"], 1):
389
355
  print(f" {i}. {cls['name']}")
390
356
  print(f" └─ {cls['file']}:{cls['line']}")
391
357
 
392
358
  if data["unused_variables"]:
393
- print("\n📊 Unused Variables")
359
+ print("\n - Unused Variables")
394
360
  print("==================")
395
361
  for i, var in enumerate(data["unused_variables"], 1):
396
362
  print(f" {i}. {var['name']}")
@@ -403,7 +369,7 @@ if __name__=="__main__":
403
369
  print(f"```")
404
370
 
405
371
  print("\nNext steps:")
406
- print(" Use --interactive to select specific items to remove")
407
- 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")
408
374
  else:
409
375
  print("Usage: python Skylos.py <path> [confidence_threshold]")