skylos 0.0.1__tar.gz → 1.0.8__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 (66) hide show
  1. skylos-1.0.8/PKG-INFO +8 -0
  2. skylos-1.0.8/README.md +233 -0
  3. skylos-1.0.8/pyproject.toml +14 -0
  4. skylos-1.0.8/setup.cfg +4 -0
  5. skylos-1.0.8/setup.py +26 -0
  6. skylos-1.0.8/skylos/__init__.py +8 -0
  7. skylos-1.0.8/skylos/analyzer.py +180 -0
  8. skylos-1.0.8/skylos/visitor.py +181 -0
  9. skylos-1.0.8/skylos.egg-info/PKG-INFO +8 -0
  10. skylos-1.0.8/skylos.egg-info/SOURCES.txt +25 -0
  11. skylos-1.0.8/skylos.egg-info/dependency_links.txt +1 -0
  12. skylos-1.0.8/skylos.egg-info/entry_points.txt +2 -0
  13. skylos-1.0.8/skylos.egg-info/requires.txt +1 -0
  14. skylos-1.0.8/skylos.egg-info/top_level.txt +2 -0
  15. skylos-1.0.8/test/compare_tools.py +604 -0
  16. skylos-1.0.8/test/diagnostics.py +364 -0
  17. skylos-1.0.8/test/sample_repo/app.py +13 -0
  18. skylos-1.0.8/test/sample_repo/sample_repo/commands.py +81 -0
  19. skylos-1.0.8/test/sample_repo/sample_repo/models.py +122 -0
  20. skylos-1.0.8/test/sample_repo/sample_repo/routes.py +89 -0
  21. skylos-1.0.8/test/sample_repo/sample_repo/utils.py +36 -0
  22. skylos-1.0.8/test/test_skylos.py +456 -0
  23. skylos-1.0.8/test/test_visitor.py +220 -0
  24. skylos-0.0.1/.DS_Store +0 -0
  25. skylos-0.0.1/.gitignore +0 -17
  26. skylos-0.0.1/Cargo.lock +0 -712
  27. skylos-0.0.1/Cargo.toml +0 -27
  28. skylos-0.0.1/License +0 -13
  29. skylos-0.0.1/PKG-INFO +0 -295
  30. skylos-0.0.1/README.md +0 -288
  31. skylos-0.0.1/build.rs +0 -57
  32. skylos-0.0.1/pyproject.toml +0 -16
  33. skylos-0.0.1/skylos/__init__.py +0 -30
  34. skylos-0.0.1/src/lib.rs +0 -1232
  35. skylos-0.0.1/src/main.rs +0 -12
  36. skylos-0.0.1/src/queries.rs +0 -34
  37. skylos-0.0.1/src/types.rs +0 -16
  38. skylos-0.0.1/src/utils.rs +0 -46
  39. skylos-0.0.1/test/benchmark/benchmarks.py +0 -259
  40. skylos-0.0.1/test/sample_project/__init__.py +0 -5
  41. skylos-0.0.1/test/sample_project/module1.py +0 -15
  42. skylos-0.0.1/test/sample_project/module2.py +0 -12
  43. skylos-0.0.1/test/sample_project/module3.py +0 -25
  44. skylos-0.0.1/test/sample_project/module4.py +0 -45
  45. skylos-0.0.1/test/sample_project/module5.py +0 -38
  46. skylos-0.0.1/test/sample_project/module6.py +0 -44
  47. skylos-0.0.1/test/sample_project/module8.py +0 -24
  48. skylos-0.0.1/test/sample_project/module_13.py +0 -15
  49. skylos-0.0.1/test/sample_project/subpackage/__init__.py +0 -5
  50. skylos-0.0.1/test/sample_project/subpackage/module3.py +0 -54
  51. skylos-0.0.1/test/sample_project/subpackage/module7.py +0 -41
  52. skylos-0.0.1/test/sample_project/subpackage2/folder1/__init__.py +0 -42
  53. skylos-0.0.1/test/sample_project/subpackage2/folder1/module11.py +0 -17
  54. skylos-0.0.1/test/sample_project/subpackage2/folder1/module12.py +0 -35
  55. skylos-0.0.1/test/sample_project/subpackage2/folder2/folder1/module10.py +0 -54
  56. skylos-0.0.1/test/test_cli.py +0 -13
  57. skylos-0.0.1/test/test_sanity.py +0 -15
  58. skylos-0.0.1/test/test_script.py +0 -2
  59. skylos-0.0.1/test/test_skylos.py +0 -24
  60. skylos-0.0.1/test/test_skylos_calls.py +0 -27
  61. skylos-0.0.1/test/test_skylos_main_block.py +0 -27
  62. skylos-0.0.1/test/test_skylos_pyapi.py +0 -9
  63. {skylos-0.0.1 → skylos-1.0.8}/skylos/cli.py +0 -0
  64. {skylos-0.0.1 → skylos-1.0.8}/test/__init__.py +0 -0
  65. {skylos-0.0.1/test/sample_project/subpackage2 → skylos-1.0.8/test/sample_repo}/__init__.py +0 -0
  66. {skylos-0.0.1/test/sample_project/subpackage2/folder2/folder1 → skylos-1.0.8/test/sample_repo/sample_repo}/__init__.py +0 -0
skylos-1.0.8/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: skylos
3
+ Version: 1.0.8
4
+ Summary: A static analysis tool for Python codebases
5
+ Author-email: oha <aaronoh2015@gmail.com>
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: inquirer>=3.0.0
8
+ Dynamic: requires-python
skylos-1.0.8/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Skylos 🔍
2
+
3
+ ![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)
4
+ ![100% Local](https://img.shields.io/badge/privacy-100%25%20local-brightgreen)
5
+ ![PyPI version](https://img.shields.io/pypi/v/skylos)
6
+
7
+ <div align="center">
8
+ <img src="assets/SKYLOS.png" alt="Skylos Logo" width="200">
9
+ </div>
10
+
11
+ > 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.
12
+
13
+ ## Features
14
+
15
+ * Unused Functions & Methods: Finds functions and methods that are never called
16
+ * Unused Classes: Detects classes that are never instantiated or inherited
17
+ * Unused Imports: Identifies imports that serve no purpose
18
+ * Cross-Module Tracking: Analyzes usage patterns across your entire codebase
19
+
20
+ ## Benchmark (You can find this benchmark test in `test` folder)
21
+
22
+ The benchmark checks how well static analysis tools spot dead code in Python. Things such as unused functions, classes, imports, variables, that kinda stuff. To read more refer down below.
23
+
24
+ **The methodology and process for benchmarking can be found in `BENCHMARK.md`**
25
+
26
+ | Tool | Time (s) | Items | TP | FP | FN | Precision | Recall | F1 Score |
27
+ |------|----------|-------|----|----|----|-----------|---------|---------|
28
+ | **Skylos (Local Dev)** | **0.013** | **34** | **22** | **12** | **7** | **0.6471** | **0.7586** | **0.6984** |
29
+ | Vulture (0%) | 0.054 | 32 | 11 | 20 | 18 | 0.3548 | 0.3793 | 0.3667 |
30
+ | Vulture (60%) | 0.044 | 32 | 11 | 20 | 18 | 0.3548 | 0.3793 | 0.3667 |
31
+ | Flake8 | 0.371 | 16 | 5 | 7 | 24 | 0.4167 | 0.1724 | 0.2439 |
32
+ | Pylint | 0.705 | 11 | 0 | 8 | 29 | 0.0000 | 0.0000 | 0.0000 |
33
+ | Ruff | 0.140 | 16 | 5 | 7 | 24 | 0.4167 | 0.1724 | 0.2439 |
34
+
35
+ To run the benchmark:
36
+ `python compare_tools.py /path/to/sample_repo`
37
+
38
+ **Note: More can be found in `BENCHMARK.md`**
39
+
40
+ ## Installation
41
+
42
+ ### Basic Installation
43
+
44
+ ```bash
45
+ pip install skylos
46
+ ```
47
+
48
+ ### From Source
49
+
50
+ ```bash
51
+ # Clone the repository
52
+ git clone https://github.com/duriantaco/skylos.git
53
+ cd skylos
54
+
55
+ ## Install your dependencies
56
+ pip install .
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```bash
62
+ # Analyze a project
63
+ skylos /path/to/your/project
64
+
65
+ # Interactive mode - select items to remove
66
+ skylos --interactive /path/to/your/project
67
+
68
+ # Dry run - see what would be removed
69
+ skylos --interactive --dry-run /path/to/your/project
70
+
71
+ # Output to JSON
72
+ skylos --json /path/to/your/project
73
+ ```
74
+
75
+ ## CLI Options
76
+
77
+ ```
78
+ Usage: skylos [OPTIONS] PATH
79
+
80
+ Arguments:
81
+ PATH Path to the Python project to analyze
82
+
83
+ Options:
84
+ -h, --help Show this help message and exit
85
+ -j, --json Output raw JSON instead of formatted text
86
+ -o, --output FILE Write output to file instead of stdout
87
+ -v, --verbose Enable verbose output
88
+ -i, --interactive Interactively select items to remove
89
+ --dry-run Show what would be removed without modifying files
90
+ ```
91
+
92
+ ## Example Output
93
+
94
+ ```
95
+ 🔍 Python Static Analysis Results
96
+ ===================================
97
+
98
+ Summary:
99
+ • Unreachable functions: 48
100
+ • Unused imports: 8
101
+
102
+ 📦 Unreachable Functions
103
+ ========================
104
+ 1. module_13.test_function
105
+ └─ /Users/oha/project/module_13.py:5
106
+ 2. module_13.unused_function
107
+ └─ /Users/oha/project/module_13.py:13
108
+ ...
109
+
110
+ 📥 Unused Imports
111
+ =================
112
+ 1. os
113
+ └─ /Users/oha/project/module_13.py:1
114
+ 2. json
115
+ └─ /Users/oha/project/module_13.py:3
116
+ ...
117
+
118
+ Next steps:
119
+ • Use --interactive to select specific items to remove
120
+ • Use --dry-run to preview changes before applying them
121
+ ```
122
+
123
+ ## Interactive Mode
124
+
125
+ The interactive mode lets you select specific functions and imports to remove:
126
+
127
+ ![Interactive Demo](docs/interactive-demo.gif)
128
+
129
+ 1. **Select items**: Use arrow keys and space to select/deselect
130
+ 2. **Confirm changes**: Review selected items before applying
131
+ 3. **Auto-cleanup**: Files are automatically updated
132
+
133
+ ## Development
134
+
135
+ ### Prerequisites
136
+
137
+ - `Python ≥3.9`
138
+ - `pytest`
139
+ - `inquirer`
140
+
141
+ ### Setup
142
+
143
+ ```bash
144
+ # Clone the repository
145
+ git clone https://github.com/duriantaco/skylos.git
146
+ cd skylos
147
+
148
+ # Create a virtual environment
149
+ python -m venv venv
150
+ source venv/bin/activate # On Windows: venv\Scripts\activate
151
+ ```
152
+
153
+ ### Running Tests
154
+
155
+ ```bash
156
+ # Run Python tests
157
+ python -m pytest tests/
158
+ ```
159
+
160
+ ## FAQ
161
+
162
+ **Q: Why doesn't Skylos find 100% of dead code?**
163
+ A: Python's dynamic features (getattr, globals, etc.) can't be perfectly analyzed statically. No tool can achieve 100% accuracy. If they say they can, they're lying.
164
+
165
+ **Q: Why are the results different on my codebase?**
166
+ A: These benchmarks use specific test cases. Your code patterns (frameworks, legacy code, etc.) will give different results.
167
+
168
+ **Q: Are these benchmarks realistic?**
169
+ A: They test common scenarios but can't cover every edge case. Use them as a guide, not gospel.
170
+
171
+ **Q: Should I automatically delete everything flagged as unused?**
172
+ A: No. Always review results manually, especially for framework code, APIs, and test utilities.
173
+
174
+ **Q: Why did Ruff underperform?**
175
+ A: Like all other tools, Ruff is focused on detecting specific, surface-level issues. Tools like Vulture and Skylos are built SPECIFICALLY for dead code detection. It is NOT a specialized dead code detector. If your goal is dead code, then ruff is the wrong tool. It is a good tool but it's like using a wrench to hammer a nail. Good tool, wrong purpose.
176
+
177
+ ## Limitations
178
+
179
+ - **Dynamic code**: `getattr()`, `globals()`, runtime imports are hard to detect
180
+ - **Frameworks**: Django models, Flask routes may appear unused but aren't
181
+ - **Test data**: Limited scenarios, your mileage may vary
182
+ - **False positives**: Always manually review before deleting code
183
+
184
+ If we can detect 100% of all dead code in any structure, we wouldn't be sitting here. But we definitely tried our best
185
+
186
+ ## Troubleshooting
187
+
188
+ ### Common Issues
189
+
190
+ 1. **Permission Errors**
191
+ ```
192
+ Error: Permission denied when removing function
193
+ ```
194
+ Check file permissions before running in interactive mode.
195
+
196
+ 2. **Missing Dependencies**
197
+ ```
198
+ Interactive mode requires 'inquirer' package
199
+ ```
200
+ Install with: `pip install skylos[interactive]`
201
+
202
+ ## Contributing
203
+
204
+ We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting pull requests.
205
+
206
+ ### Quick Contribution Guide
207
+
208
+ 1. Fork the repository
209
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
210
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
211
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
212
+ 5. Open a Pull Request
213
+
214
+ ## Roadmap
215
+
216
+ - [ ] Expand our test cases
217
+ - [ ] Configuration file support
218
+ - [ ] Custom analysis rules
219
+ - [ ] Git hooks integration
220
+ - [ ] CI/CD integration examples
221
+ - [ ] Web interface
222
+ - [ ] Support for other languages
223
+ - [ ] Further optimization
224
+
225
+ ## License
226
+
227
+ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
228
+
229
+ ## Contact
230
+
231
+ - **Author**: oha
232
+ - **Email**: aaronoh2015@gmail.com
233
+ - **GitHub**: [@duriantaco](https://github.com/duriantaco)
@@ -0,0 +1,14 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "skylos"
7
+ version = "1.0.8"
8
+ requires-python = ">=3.9"
9
+ description = "A static analysis tool for Python codebases"
10
+ authors = [{name = "oha", email = "aaronoh2015@gmail.com"}]
11
+ dependencies = ["inquirer>=3.0.0"]
12
+
13
+ [project.scripts]
14
+ skylos = "skylos.cli:main"
skylos-1.0.8/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
skylos-1.0.8/setup.py ADDED
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="skylos",
5
+ version="1.0.8",
6
+ packages=find_packages(),
7
+ python_requires=">=3.9",
8
+ install_requires=["inquirer>=3.0.0"],
9
+ classifiers=[
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: Apache 2.0 License",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Topic :: Software Development :: Quality Assurance",
19
+ "Topic :: Software Development :: Testing",
20
+ ],
21
+ entry_points={
22
+ "console_scripts": [
23
+ "skylos=skylos.cli:main",
24
+ ],
25
+ },
26
+ )
@@ -0,0 +1,8 @@
1
+ from skylos.analyzer import analyze
2
+
3
+ __version__ = "1.0.8"
4
+
5
+ def debug_test():
6
+ return "debug-ok"
7
+
8
+ __all__ = ["analyze", "debug_test", "__version__"]
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ import ast,sys,json,logging,re
3
+ from pathlib import Path
4
+ from collections import defaultdict
5
+ from skylos.visitor import Visitor
6
+
7
+ logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
8
+ logger=logging.getLogger('Skylos')
9
+
10
+ AUTO_CALLED={"__init__","__enter__","__exit__"}
11
+ TEST_BASE_CLASSES = {"TestCase", "AsyncioTestCase", "unittest.TestCase", "unittest.AsyncioTestCase"}
12
+ TEST_METHOD_PATTERN = re.compile(r"^test_\w+$")
13
+ MAGIC_METHODS={f"__{n}__"for n in["init","new","call","getattr","getattribute","enter","exit","str","repr","hash","eq","ne","lt","gt","le","ge","iter","next","contains","len","getitem","setitem","delitem","iadd","isub","imul","itruediv","ifloordiv","imod","ipow","ilshift","irshift","iand","ixor","ior","round","format","dir","abs","complex","int","float","bool","bytes","reduce","await","aiter","anext","add","sub","mul","truediv","floordiv","mod","divmod","pow","lshift","rshift","and","or","xor","radd","rsub","rmul","rtruediv","rfloordiv","rmod","rdivmod","rpow","rlshift","rrshift","rand","ror","rxor"]}
14
+
15
+ class Skylos:
16
+ def __init__(self):
17
+ self.defs={}
18
+ self.refs=[]
19
+ self.dynamic=set()
20
+ self.exports=defaultdict(set)
21
+
22
+ def _module(self,root,f):
23
+ p=list(f.relative_to(root).parts)
24
+ if p[-1].endswith(".py"):p[-1]=p[-1][:-3]
25
+ if p[-1]=="__init__":p.pop()
26
+ return".".join(p)
27
+
28
+ def _mark_exports(self):
29
+
30
+ for name, d in self.defs.items():
31
+ if d.in_init and not d.simple_name.startswith('_'):
32
+ d.is_exported = True
33
+
34
+ for mod, export_names in self.exports.items():
35
+ for name in export_names:
36
+ for def_name, def_obj in self.defs.items():
37
+ if (def_name.startswith(f"{mod}.") and
38
+ def_obj.simple_name == name and
39
+ def_obj.type != "import"):
40
+ def_obj.is_exported = True
41
+
42
+ def _mark_refs(self):
43
+ import_to_original = {}
44
+ for name, def_obj in self.defs.items():
45
+ if def_obj.type == "import":
46
+ import_name = name.split('.')[-1]
47
+
48
+ for def_name, orig_def in self.defs.items():
49
+ if (orig_def.type != "import" and
50
+ orig_def.simple_name == import_name and
51
+ def_name != name):
52
+ import_to_original[name] = def_name
53
+ break
54
+
55
+ simple_name_lookup = defaultdict(list)
56
+ for d in self.defs.values():
57
+ simple_name_lookup[d.simple_name].append(d)
58
+
59
+ for ref, file in self.refs:
60
+ if ref in self.defs:
61
+ self.defs[ref].references += 1
62
+
63
+ if ref in import_to_original:
64
+ original = import_to_original[ref]
65
+ self.defs[original].references += 1
66
+ continue
67
+
68
+ simple = ref.split('.')[-1]
69
+ matches = simple_name_lookup.get(simple, [])
70
+ for d in matches:
71
+ d.references += 1
72
+
73
+ def _get_base_classes(self, class_name):
74
+ """Get base classes for a given class name"""
75
+ if class_name not in self.defs:
76
+ return []
77
+
78
+ class_def = self.defs[class_name]
79
+
80
+ if hasattr(class_def, 'base_classes'):
81
+ return class_def.base_classes
82
+
83
+ return []
84
+
85
+ def _apply_heuristics(self):
86
+
87
+ class_methods=defaultdict(list)
88
+ for d in self.defs.values():
89
+ if d.type in("method","function") and"." in d.name:
90
+ cls=d.name.rsplit(".",1)[0]
91
+ if cls in self.defs and self.defs[cls].type=="class":
92
+ class_methods[cls].append(d)
93
+
94
+ for cls,methods in class_methods.items():
95
+ if self.defs[cls].references>0:
96
+ for m in methods:
97
+ if m.simple_name in AUTO_CALLED:m.references+=1
98
+
99
+ for d in self.defs.values():
100
+ if d.simple_name in MAGIC_METHODS or d.simple_name.startswith("__")and d.simple_name.endswith("__"):d.confidence=0
101
+ if not d.simple_name.startswith("_")and d.type in("function","method","class"):d.confidence=min(d.confidence,90)
102
+ if d.in_init and d.type in("function","class"):d.confidence=min(d.confidence,85)
103
+ if d.name.split(".")[0] in self.dynamic:d.confidence=min(d.confidence,50)
104
+
105
+ for d in self.defs.values():
106
+ if d.type == "method" and TEST_METHOD_PATTERN.match(d.simple_name):
107
+ # check if its in a class that inherits from a test base class
108
+ class_name = d.name.rsplit(".", 1)[0]
109
+ class_simple_name = class_name.split(".")[-1]
110
+ # class name suggests it's a test class, ignore test methods
111
+ if "Test" in class_simple_name or class_simple_name.endswith("TestCase"):
112
+ d.confidence = 0
113
+
114
+ def analyze(self, path, thr=60):
115
+ p = Path(path).resolve()
116
+ files = [p] if p.is_file() else list(p.glob("**/*.py"))
117
+ root = p.parent if p.is_file() else p
118
+
119
+ modmap = {}
120
+ for f in files:
121
+ modmap[f] = self._module(root, f)
122
+
123
+ for file in files:
124
+ mod = modmap[file]
125
+ defs, refs, dyn, exports = proc_file(file, mod)
126
+
127
+ for d in defs:
128
+ self.defs[d.name] = d
129
+ self.refs.extend(refs)
130
+ self.dynamic.update(dyn)
131
+ self.exports[mod].update(exports)
132
+
133
+ self._mark_refs()
134
+ self._apply_heuristics()
135
+ self._mark_exports()
136
+
137
+ # for name, d in self.defs.items():
138
+ # print(f" {d.type} '{name}': {d.references} refs, exported: {d.is_exported}, confidence: {d.confidence}")
139
+
140
+ thr = max(0, thr)
141
+
142
+ unused = []
143
+ for d in self.defs.values():
144
+ if d.references == 0 and not d.is_exported and d.confidence >= thr:
145
+ unused.append(d.to_dict())
146
+
147
+ result = {"unused_functions": [], "unused_imports": [], "unused_classes": []}
148
+ for u in unused:
149
+ if u["type"] in ("function", "method"):
150
+ result["unused_functions"].append(u)
151
+ elif u["type"] == "import":
152
+ result["unused_imports"].append(u)
153
+ elif u["type"] == "class":
154
+ result["unused_classes"].append(u)
155
+
156
+ return json.dumps(result, indent=2)
157
+
158
+ def proc_file(file_or_args, mod=None):
159
+ if mod is None and isinstance(file_or_args, tuple):
160
+ file, mod = file_or_args
161
+ else:
162
+ file = file_or_args
163
+
164
+ try:
165
+ tree = ast.parse(Path(file).read_text(encoding="utf-8"))
166
+ v = Visitor(mod, file)
167
+ v.visit(tree)
168
+ return v.defs, v.refs, v.dyn, v.exports
169
+ except Exception as e:
170
+ logger.error(f"{file}: {e}")
171
+ return [], [], set(), set()
172
+
173
+ def analyze(path,conf=60):return Skylos().analyze(path,conf)
174
+
175
+ if __name__=="__main__":
176
+ if len(sys.argv)>1:
177
+ p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
178
+ print(analyze(p,c))
179
+ else:
180
+ print("Usage: python Skylos.py <path> [confidence_threshold]")
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ import ast,re
3
+ from pathlib import Path
4
+
5
+ PYTHON_BUILTINS={"print","len","str","int","float","list","dict","set","tuple","range","open","super","object","type","enumerate","zip","map","filter","sorted","reversed","sum","min","max","all","any","next","iter","repr","chr","ord","bytes","bytearray","memoryview","format","round","abs","pow","divmod","complex","hash","id","bool","callable","getattr","setattr","delattr","hasattr","isinstance","issubclass","globals","locals","vars","dir","property","classmethod","staticmethod"}
6
+ DYNAMIC_PATTERNS={"getattr","globals","eval","exec"}
7
+
8
+ class Definition:
9
+ __slots__ = ('name', 'type', 'filename', 'line', 'simple_name', 'confidence', 'references', 'is_exported', 'in_init')
10
+
11
+ def __init__(self, n, t, f, l):
12
+ self.name = n
13
+ self.type = t
14
+ self.filename = f
15
+ self.line = l
16
+ self.simple_name = n.split('.')[-1]
17
+ self.confidence = 100
18
+ self.references = 0
19
+ self.is_exported = False
20
+ self.in_init = "__init__.py" in str(f)
21
+
22
+ def to_dict(self):
23
+ if self.type == "method" and "." in self.name:
24
+ parts = self.name.split(".")
25
+ if len(parts) >= 3:
26
+ output_name = ".".join(parts[-2:])
27
+ else:
28
+ output_name = self.name
29
+ else:
30
+ output_name = self.simple_name
31
+
32
+ return{
33
+ "name": output_name,
34
+ "full_name": self.name,
35
+ "simple_name": self.simple_name,
36
+ "type": self.type,
37
+ "file": str(self.filename),
38
+ "basename": Path(self.filename).name,
39
+ "line": self.line,
40
+ "confidence": self.confidence,
41
+ "references": self.references
42
+ }
43
+
44
+ class Visitor(ast.NodeVisitor):
45
+ def __init__(self,mod,file):
46
+ self.mod=mod
47
+ self.file=file
48
+ self.defs=[]
49
+ self.refs=[]
50
+ self.cls=None
51
+ self.alias={}
52
+ self.dyn=set()
53
+ self.exports=set()
54
+ self.current_function_scope = []
55
+
56
+ def add_def(self,n,t,l):
57
+ if n not in{d.name for d in self.defs}:self.defs.append(Definition(n,t,self.file,l))
58
+
59
+ def add_ref(self,n):self.refs.append((n,self.file))
60
+
61
+ def qual(self,n):
62
+ if n in self.alias:return self.alias[n]
63
+ if n in PYTHON_BUILTINS:return n
64
+ return f"{self.mod}.{n}"if self.mod else n
65
+
66
+ def visit_Import(self,node):
67
+ for a in node.names:
68
+ full=a.name
69
+ self.alias[a.asname or a.name.split(".")[-1]]=full
70
+ self.add_def(full,"import",node.lineno)
71
+
72
+ def visit_ImportFrom(self,node):
73
+ if node.module is None:return
74
+ for a in node.names:
75
+ if a.name=="*":continue
76
+ base=node.module
77
+ if node.level:
78
+ parts=self.mod.split(".")
79
+ base=".".join(parts[:-node.level])+(f".{node.module}"if node.module else"")
80
+ full=f"{base}.{a.name}"
81
+ self.alias[a.asname or a.name]=full
82
+ self.add_def(full,"import",node.lineno)
83
+
84
+ def visit_FunctionDef(self,node):
85
+ outer_scope_prefix = '.'.join(self.current_function_scope) + '.' if self.current_function_scope else ''
86
+
87
+ if self.cls:
88
+ name_parts = [self.mod, self.cls, outer_scope_prefix + node.name]
89
+ else:
90
+ name_parts = [self.mod, outer_scope_prefix + node.name]
91
+
92
+ qualified_name = ".".join(filter(None, name_parts))
93
+
94
+ self.add_def(qualified_name,"method"if self.cls else"function",node.lineno)
95
+
96
+ self.current_function_scope.append(node.name)
97
+ for d_node in node.decorator_list:
98
+ self.visit(d_node)
99
+ for stmt in node.body:
100
+ self.visit(stmt)
101
+ self.current_function_scope.pop()
102
+
103
+ visit_AsyncFunctionDef=visit_FunctionDef
104
+
105
+ def visit_ClassDef(self,node):
106
+ cname=f"{self.mod}.{node.name}"
107
+ self.add_def(cname,"class",node.lineno)
108
+ prev=self.cls;self.cls=node.name
109
+ for b in node.body:self.visit(b)
110
+ self.cls=prev
111
+
112
+ def visit_Assign(self, node):
113
+ for target in node.targets:
114
+ if isinstance(target, ast.Name) and target.id == "__all__":
115
+ if isinstance(node.value, (ast.List, ast.Tuple)):
116
+ for elt in node.value.elts:
117
+ value = None
118
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
119
+ value = elt.value
120
+ elif hasattr(elt, 's') and isinstance(elt.s, str):
121
+ value = elt.s
122
+
123
+ if value is not None:
124
+ full_name = f"{self.mod}.{value}"
125
+ self.add_ref(full_name)
126
+ self.add_ref(value)
127
+ self.generic_visit(node)
128
+
129
+ def visit_Call(self, node):
130
+ self.generic_visit(node)
131
+
132
+ if isinstance(node.func, ast.Name) and node.func.id in ("getattr", "hasattr") and len(node.args) >= 2:
133
+ if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
134
+ attr_name = node.args[1].value
135
+ self.add_ref(attr_name)
136
+
137
+ if isinstance(node.args[0], ast.Name):
138
+ module_name = node.args[0].id
139
+ if module_name != "self":
140
+ qualified_name = f"{self.qual(module_name)}.{attr_name}"
141
+ self.add_ref(qualified_name)
142
+
143
+ elif isinstance(node.func, ast.Name) and node.func.id == "globals":
144
+ parent = getattr(node, 'parent', None)
145
+ if (isinstance(parent, ast.Subscript) and
146
+ isinstance(parent.slice, ast.Constant) and
147
+ isinstance(parent.slice.value, str)):
148
+ func_name = parent.slice.value
149
+ self.add_ref(func_name)
150
+ self.add_ref(f"{self.mod}.{func_name}")
151
+
152
+ elif (isinstance(node.func, ast.Attribute) and
153
+ node.func.attr == "format" and
154
+ isinstance(node.func.value, ast.Constant) and
155
+ isinstance(node.func.value.value, str)):
156
+ fmt = node.func.value.value
157
+ if any(isinstance(k.arg, str) and k.arg is None for k in node.keywords):
158
+ for _, n, _, _ in re.findall(r'\{([^}:!]+)', fmt):
159
+ if n:
160
+ self.add_ref(self.qual(n))
161
+
162
+ def visit_Name(self,node):
163
+ if isinstance(node.ctx,ast.Load):
164
+ self.add_ref(self.qual(node.id))
165
+ if node.id in DYNAMIC_PATTERNS:self.dyn.add(self.mod.split(".")[0])
166
+
167
+ def visit_Attribute(self,node):
168
+ self.generic_visit(node)
169
+ if isinstance(node.ctx,ast.Load)and isinstance(node.value,ast.Name):
170
+ self.add_ref(f"{self.qual(node.value.id)}.{node.attr}")
171
+
172
+ def generic_visit(self, node):
173
+ for field, value in ast.iter_fields(node):
174
+ if isinstance(value, list):
175
+ for item in value:
176
+ if isinstance(item, ast.AST):
177
+ item.parent = node
178
+ self.visit(item)
179
+ elif isinstance(value, ast.AST):
180
+ value.parent = node
181
+ self.visit(value)
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: skylos
3
+ Version: 1.0.8
4
+ Summary: A static analysis tool for Python codebases
5
+ Author-email: oha <aaronoh2015@gmail.com>
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: inquirer>=3.0.0
8
+ Dynamic: requires-python