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.
- {skylos-1.2.2 → skylos-2.0.0}/PKG-INFO +1 -1
- {skylos-1.2.2 → skylos-2.0.0}/README.md +34 -0
- {skylos-1.2.2 → skylos-2.0.0}/pyproject.toml +1 -1
- {skylos-1.2.2 → skylos-2.0.0}/setup.py +5 -2
- {skylos-1.2.2 → skylos-2.0.0}/skylos/__init__.py +1 -1
- {skylos-1.2.2 → skylos-2.0.0}/skylos/analyzer.py +58 -95
- {skylos-1.2.2 → skylos-2.0.0}/skylos/cli.py +46 -36
- {skylos-1.2.2 → skylos-2.0.0}/skylos/constants.py +25 -10
- {skylos-1.2.2 → skylos-2.0.0}/skylos/framework_aware.py +8 -2
- skylos-2.0.0/skylos/server.py +560 -0
- {skylos-1.2.2 → skylos-2.0.0}/skylos/test_aware.py +0 -1
- {skylos-1.2.2 → skylos-2.0.0}/skylos/visitor.py +70 -56
- {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/PKG-INFO +1 -1
- {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/SOURCES.txt +1 -0
- {skylos-1.2.2 → skylos-2.0.0}/setup.cfg +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/requires.txt +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/skylos.egg-info/top_level.txt +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/__init__.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/compare_tools.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/conftest.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/diagnostics.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/__init__.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/app.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/__init__.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_analyzer.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_changes_analyzer.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_cli.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_constants.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_framework_aware.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_integration.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_skylos.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_test_aware.py +0 -0
- {skylos-1.2.2 → skylos-2.0.0}/test/test_visitor.py +0 -0
|
@@ -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
|
|
@@ -2,10 +2,13 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="skylos",
|
|
5
|
-
version="
|
|
5
|
+
version="2.0.0",
|
|
6
6
|
packages=find_packages(),
|
|
7
7
|
python_requires=">=3.9",
|
|
8
|
-
install_requires=[
|
|
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",
|
|
@@ -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"):
|
|
62
|
-
|
|
63
|
-
|
|
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,
|
|
115
|
-
if
|
|
116
|
-
|
|
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
|
|
141
|
-
simple_name_lookup[
|
|
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
|
|
155
|
-
|
|
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
|
-
|
|
176
|
-
|
|
158
|
+
confidence=100
|
|
177
159
|
if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
|
|
178
|
-
|
|
160
|
+
confidence -= PENALTIES["private_name"]
|
|
179
161
|
if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
|
|
180
|
-
|
|
162
|
+
confidence -= PENALTIES["dunder_or_magic"]
|
|
181
163
|
if def_obj.type == "variable" and def_obj.simple_name == "_":
|
|
182
|
-
|
|
164
|
+
confidence -= PENALTIES["underscored_var"]
|
|
183
165
|
if def_obj.in_init and def_obj.type in ("function", "class"):
|
|
184
|
-
|
|
166
|
+
confidence -= PENALTIES["in_init_file"]
|
|
185
167
|
if def_obj.name.split(".")[0] in self.dynamic:
|
|
186
|
-
|
|
168
|
+
confidence -= PENALTIES["dynamic_module"]
|
|
187
169
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
188
|
-
|
|
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
|
-
|
|
174
|
+
confidence = min(confidence, framework_confidence)
|
|
193
175
|
|
|
194
176
|
if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
|
|
195
|
-
|
|
177
|
+
confidence = 0
|
|
196
178
|
|
|
197
179
|
if def_obj.type == "parameter":
|
|
198
180
|
if def_obj.simple_name in ("self", "cls"):
|
|
199
|
-
|
|
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
|
-
|
|
185
|
+
confidence = 0
|
|
204
186
|
|
|
205
187
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
206
|
-
|
|
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
|
-
|
|
193
|
+
confidence = 0
|
|
212
194
|
|
|
213
|
-
def_obj.confidence = max(
|
|
195
|
+
def_obj.confidence = max(confidence, 0)
|
|
214
196
|
|
|
215
197
|
def _apply_heuristics(self):
|
|
216
198
|
class_methods = defaultdict(list)
|
|
217
|
-
for
|
|
218
|
-
if
|
|
219
|
-
cls =
|
|
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(
|
|
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
|
|
226
|
-
if
|
|
227
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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
|
|
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]
|
|
356
|
-
|
|
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
|
|
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"
|
|
329
|
+
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
367
330
|
if data["unused_imports"]:
|
|
368
|
-
print(f"
|
|
331
|
+
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
369
332
|
if data["unused_classes"]:
|
|
370
|
-
print(f"
|
|
333
|
+
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
371
334
|
if data["unused_variables"]:
|
|
372
|
-
print(f"
|
|
335
|
+
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
373
336
|
|
|
374
337
|
if data["unused_functions"]:
|
|
375
|
-
print("\n
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
410
|
-
print("
|
|
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]")
|