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 +1 -1
- skylos/analyzer.py +103 -121
- skylos/cli.py +66 -109
- skylos/codemods.py +89 -0
- skylos/constants.py +25 -10
- skylos/framework_aware.py +290 -90
- skylos/server.py +560 -0
- skylos/test_aware.py +0 -1
- skylos/visitor.py +249 -90
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/METADATA +4 -1
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/RECORD +16 -13
- test/test_codemods.py +153 -0
- test/test_framework_aware.py +176 -242
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/WHEEL +0 -0
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/entry_points.txt +0 -0
- {skylos-1.2.2.dist-info → skylos-2.1.0.dist-info}/top_level.txt +0 -0
skylos/framework_aware.py
CHANGED
|
@@ -2,65 +2,46 @@ import ast
|
|
|
2
2
|
import fnmatch
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
# decorator patterns for each of the more popular frameworks
|
|
6
5
|
FRAMEWORK_DECORATORS = [
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"@router.get", "@router.post", "@router.put", "@router.delete",
|
|
14
|
-
"@router.patch", "@router.head", "@router.options",
|
|
15
|
-
"@router.trace", "@router.websocket",
|
|
16
|
-
|
|
17
|
-
# django
|
|
18
|
-
"@*_required", "@login_required", "@permission_required",
|
|
19
|
-
"@user_passes_test", "@staff_member_required",
|
|
20
|
-
|
|
21
|
-
# pydantic
|
|
6
|
+
"@*.route", "@*.get", "@*.post", "@*.put", "@*.delete", "@*.patch",
|
|
7
|
+
"@*.before_request", "@*.after_request", "@*.errorhandler", "@*.teardown_*",
|
|
8
|
+
"@*.head", "@*.options", "@*.trace", "@*.websocket",
|
|
9
|
+
"@*.middleware", "@*.on_event", "@*.exception_handler",
|
|
10
|
+
"@*_required", "@login_required", "@permission_required", "django.views.decorators.*",
|
|
11
|
+
"@*.simple_tag", "@*.inclusion_tag",
|
|
22
12
|
"@validator", "@field_validator", "@model_validator", "@root_validator",
|
|
23
|
-
|
|
24
|
-
# celery patterns
|
|
25
|
-
"@task", "@shared_task", "@periodic_task", "@celery_task",
|
|
26
|
-
|
|
27
|
-
# generic ones
|
|
28
|
-
"@route", "@get", "@post", "@put", "@delete", "@patch",
|
|
29
|
-
"@middleware", "@depends", "@inject"
|
|
13
|
+
"@field_serializer", "@model_serializer", "@computed_field",
|
|
30
14
|
]
|
|
31
15
|
|
|
32
16
|
FRAMEWORK_FUNCTIONS = [
|
|
33
|
-
# django view methods
|
|
34
17
|
"get", "post", "put", "patch", "delete", "head", "options", "trace",
|
|
35
18
|
"*_queryset", "get_queryset", "get_object", "get_context_data",
|
|
36
19
|
"*_form", "form_valid", "form_invalid", "get_form_*",
|
|
37
|
-
|
|
38
|
-
# django model methods
|
|
39
|
-
"save", "delete", "clean", "full_clean", "*_delete", "*_save",
|
|
40
|
-
|
|
41
|
-
# generic API endpoints
|
|
42
|
-
"create_*", "update_*", "delete_*", "list_*", "retrieve_*",
|
|
43
|
-
"handle_*", "process_*", "*_handler", "*_view"
|
|
44
20
|
]
|
|
45
21
|
|
|
46
22
|
FRAMEWORK_IMPORTS = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'falcon', 'hug', 'responder', 'quart', 'hypercorn', 'uvicorn'
|
|
23
|
+
"flask", "fastapi", "django", "rest_framework", "pydantic", "celery",
|
|
24
|
+
"starlette", "uvicorn"
|
|
50
25
|
}
|
|
51
26
|
|
|
52
27
|
class FrameworkAwareVisitor:
|
|
53
|
-
|
|
54
|
-
def __init__(self, filename=None):
|
|
28
|
+
def __init__(self, filename = None):
|
|
55
29
|
self.is_framework_file = False
|
|
56
|
-
self.framework_decorated_lines = set()
|
|
57
30
|
self.detected_frameworks = set()
|
|
58
|
-
|
|
31
|
+
self.framework_decorated_lines = set()
|
|
32
|
+
self.func_defs = {}
|
|
33
|
+
self.class_defs = {}
|
|
34
|
+
self.class_method_lines = {}
|
|
35
|
+
self.pydantic_models = set()
|
|
36
|
+
self._mark_functions = set()
|
|
37
|
+
self._mark_classes = set()
|
|
38
|
+
self._mark_cbv_http_methods = set()
|
|
39
|
+
self._type_refs_in_routes = set()
|
|
59
40
|
if filename:
|
|
60
41
|
self._check_framework_imports_in_file(filename)
|
|
61
42
|
|
|
62
43
|
def visit(self, node):
|
|
63
|
-
method =
|
|
44
|
+
method = "visit_" + node.__class__.__name__
|
|
64
45
|
visitor = getattr(self, method, self.generic_visit)
|
|
65
46
|
return visitor(node)
|
|
66
47
|
|
|
@@ -73,86 +54,305 @@ class FrameworkAwareVisitor:
|
|
|
73
54
|
elif isinstance(value, ast.AST):
|
|
74
55
|
self.visit(value)
|
|
75
56
|
|
|
76
|
-
def visit_Import(self, node):
|
|
57
|
+
def visit_Import(self, node: ast.Import):
|
|
77
58
|
for alias in node.names:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
name = alias.name.lower()
|
|
60
|
+
|
|
61
|
+
for fw in FRAMEWORK_IMPORTS:
|
|
62
|
+
if fw in name:
|
|
63
|
+
self.is_framework_file = True
|
|
64
|
+
framework_name = name.split(".")[0]
|
|
65
|
+
self.detected_frameworks.add(framework_name)
|
|
66
|
+
break
|
|
67
|
+
|
|
81
68
|
self.generic_visit(node)
|
|
82
69
|
|
|
83
|
-
def visit_ImportFrom(self, node):
|
|
70
|
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
84
71
|
if node.module:
|
|
85
|
-
module_name = node.module.split(
|
|
72
|
+
module_name = node.module.split(".")[0].lower()
|
|
86
73
|
if module_name in FRAMEWORK_IMPORTS:
|
|
87
74
|
self.is_framework_file = True
|
|
88
75
|
self.detected_frameworks.add(module_name)
|
|
89
76
|
self.generic_visit(node)
|
|
90
77
|
|
|
91
|
-
def visit_FunctionDef(self, node):
|
|
78
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
79
|
+
self.func_defs.setdefault(node.name, node.lineno)
|
|
92
80
|
for deco in node.decorator_list:
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
d = self._normalize_decorator(deco)
|
|
82
|
+
|
|
83
|
+
if self._matches_framework_pattern(d, FRAMEWORK_DECORATORS):
|
|
95
84
|
self.framework_decorated_lines.add(node.lineno)
|
|
96
85
|
self.is_framework_file = True
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if self.is_framework_file:
|
|
86
|
+
|
|
87
|
+
if self._decorator_base_name_is(deco, "receiver"):
|
|
100
88
|
self.framework_decorated_lines.add(node.lineno)
|
|
101
|
-
|
|
89
|
+
self.is_framework_file = True
|
|
90
|
+
|
|
91
|
+
defaults_to_scan = []
|
|
92
|
+
if node.args.defaults:
|
|
93
|
+
defaults_to_scan.extend(node.args.defaults)
|
|
94
|
+
if node.args.kw_defaults:
|
|
95
|
+
defaults_to_scan.extend(node.args.kw_defaults)
|
|
96
|
+
|
|
97
|
+
for default in defaults_to_scan:
|
|
98
|
+
self._scan_for_depends(default)
|
|
99
|
+
|
|
100
|
+
is_route = False
|
|
101
|
+
if node.lineno in self.framework_decorated_lines:
|
|
102
|
+
is_route = True
|
|
103
|
+
|
|
104
|
+
if is_route:
|
|
105
|
+
self._collect_annotation_type_refs(node)
|
|
102
106
|
self.generic_visit(node)
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
self.visit_FunctionDef(node)
|
|
108
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
106
109
|
|
|
107
|
-
def visit_ClassDef(self, node):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
def visit_ClassDef(self, node: ast.ClassDef):
|
|
111
|
+
self.class_defs[node.name] = node
|
|
112
|
+
for item in node.body:
|
|
113
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
114
|
+
self.class_method_lines[(node.name, item.name)] = item.lineno
|
|
115
|
+
bases = self._base_names(node)
|
|
116
|
+
|
|
117
|
+
is_view_like = False
|
|
118
|
+
for base in bases:
|
|
119
|
+
for token in ("view", "viewset", "apiview", "handler"):
|
|
120
|
+
if token in base:
|
|
121
|
+
is_view_like = True
|
|
122
|
+
break
|
|
123
|
+
if is_view_like:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
is_pydantic = False
|
|
127
|
+
for base in bases:
|
|
128
|
+
if "basemodel" in base:
|
|
129
|
+
is_pydantic = True
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
if is_view_like:
|
|
133
|
+
self.is_framework_file = True
|
|
134
|
+
self._mark_cbv_http_methods.add(node.name)
|
|
135
|
+
if is_pydantic:
|
|
136
|
+
self.pydantic_models.add(node.name)
|
|
137
|
+
self.is_framework_file = True
|
|
114
138
|
self.generic_visit(node)
|
|
115
139
|
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
def visit_Assign(self, node: ast.Assign):
|
|
141
|
+
targets = []
|
|
142
|
+
for t in node.targets:
|
|
143
|
+
if isinstance(t, ast.Name):
|
|
144
|
+
targets.append(t.id)
|
|
145
|
+
|
|
146
|
+
if "urlpatterns" in targets:
|
|
147
|
+
self.is_framework_file = True
|
|
148
|
+
for elt in self._iter_list_elts(node.value):
|
|
149
|
+
if isinstance(elt, ast.Call) and self._call_name_endswith(elt, {"path", "re_path"}):
|
|
150
|
+
view_expr = self._get_posarg(elt, 1)
|
|
151
|
+
self._mark_view_from_url_pattern(view_expr)
|
|
152
|
+
self.generic_visit(node)
|
|
127
153
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
def visit_Call(self, node: ast.Call):
|
|
155
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr == "register":
|
|
156
|
+
if len(node.args) >= 2:
|
|
157
|
+
vs = node.args[1]
|
|
158
|
+
cls_name = self._simple_name(vs)
|
|
159
|
+
if cls_name:
|
|
160
|
+
self._mark_classes.add(cls_name)
|
|
161
|
+
self._mark_cbv_http_methods.add(cls_name)
|
|
162
|
+
self.is_framework_file = True
|
|
163
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr == "connect" and node.args:
|
|
164
|
+
func_name = self._simple_name(node.args[0])
|
|
165
|
+
if func_name:
|
|
166
|
+
self._mark_functions.add(func_name)
|
|
167
|
+
self.is_framework_file = True
|
|
168
|
+
self.generic_visit(node)
|
|
169
|
+
|
|
170
|
+
def finalize(self):
|
|
171
|
+
for fname in self._mark_functions:
|
|
172
|
+
if fname in self.func_defs:
|
|
173
|
+
self.framework_decorated_lines.add(self.func_defs[fname])
|
|
174
|
+
for cname in self._mark_classes:
|
|
175
|
+
cls_node = self.class_defs.get(cname)
|
|
176
|
+
if cls_node is not None:
|
|
177
|
+
self.framework_decorated_lines.add(cls_node.lineno)
|
|
178
|
+
for cname in self._mark_cbv_http_methods:
|
|
179
|
+
for meth in ("get", "post", "put", "patch", "delete", "head", "options", "trace", "list", "create", "retrieve", "update", "partial_update", "destroy"):
|
|
180
|
+
lino = self.class_method_lines.get((cname, meth))
|
|
181
|
+
if lino:
|
|
182
|
+
self.framework_decorated_lines.add(lino)
|
|
183
|
+
|
|
184
|
+
typed_models = set()
|
|
185
|
+
for t in self._type_refs_in_routes:
|
|
186
|
+
if t in self.pydantic_models:
|
|
187
|
+
typed_models.add(t)
|
|
188
|
+
|
|
189
|
+
self._mark_classes.update(typed_models)
|
|
190
|
+
for cname in typed_models:
|
|
191
|
+
cls_node = self.class_defs.get(cname)
|
|
192
|
+
if cls_node is not None:
|
|
193
|
+
self.framework_decorated_lines.add(cls_node.lineno)
|
|
131
194
|
|
|
132
195
|
def _check_framework_imports_in_file(self, filename):
|
|
133
196
|
try:
|
|
134
|
-
content = Path(filename).read_text(encoding=
|
|
197
|
+
content = Path(filename).read_text(encoding="utf-8")
|
|
198
|
+
|
|
135
199
|
for framework in FRAMEWORK_IMPORTS:
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
import_statement = f"import {framework}"
|
|
201
|
+
from_statement = f"from {framework}"
|
|
202
|
+
|
|
203
|
+
has_import = import_statement in content
|
|
204
|
+
has_from_import = from_statement in content
|
|
205
|
+
|
|
206
|
+
if has_import or has_from_import:
|
|
138
207
|
self.is_framework_file = True
|
|
139
208
|
self.detected_frameworks.add(framework)
|
|
140
209
|
break
|
|
141
|
-
|
|
210
|
+
|
|
211
|
+
except Exception:
|
|
142
212
|
pass
|
|
143
213
|
|
|
144
|
-
def
|
|
214
|
+
def _normalize_decorator(self, dec: ast.AST):
|
|
215
|
+
if isinstance(dec, ast.Call):
|
|
216
|
+
return self._normalize_decorator(dec.func)
|
|
217
|
+
if isinstance(dec, ast.Name):
|
|
218
|
+
return f"@{dec.id}"
|
|
219
|
+
if isinstance(dec, ast.Attribute):
|
|
220
|
+
return f"@{self._attr_to_str(dec)}"
|
|
221
|
+
return "@unknown"
|
|
222
|
+
|
|
223
|
+
def _matches_framework_pattern(self, text, patterns):
|
|
224
|
+
text_clean = text.lstrip("@")
|
|
225
|
+
|
|
226
|
+
for pattern in patterns:
|
|
227
|
+
pattern_clean = pattern.lstrip("@")
|
|
228
|
+
if fnmatch.fnmatch(text_clean, pattern_clean):
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
def _decorator_base_name_is(self, dec: ast.AST, name):
|
|
234
|
+
if isinstance(dec, ast.Call):
|
|
235
|
+
dec = dec.func
|
|
236
|
+
if isinstance(dec, ast.Name):
|
|
237
|
+
return dec.id == name
|
|
238
|
+
if isinstance(dec, ast.Attribute):
|
|
239
|
+
return dec.attr == name or self._attr_to_str(dec).endswith("." + name)
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def _attr_to_str(self, node: ast.Attribute):
|
|
243
|
+
parts = []
|
|
244
|
+
cur = node
|
|
245
|
+
while isinstance(cur, ast.Attribute):
|
|
246
|
+
parts.append(cur.attr)
|
|
247
|
+
cur = cur.value
|
|
248
|
+
if isinstance(cur, ast.Name):
|
|
249
|
+
parts.append(cur.id)
|
|
250
|
+
|
|
251
|
+
parts.reverse()
|
|
252
|
+
return ".".join(parts)
|
|
253
|
+
|
|
254
|
+
def _base_names(self, node: ast.ClassDef):
|
|
255
|
+
out = []
|
|
256
|
+
for b in node.bases:
|
|
257
|
+
if isinstance(b, ast.Name):
|
|
258
|
+
out.append(b.id.lower())
|
|
259
|
+
elif isinstance(b, ast.Attribute):
|
|
260
|
+
out.append(self._attr_to_str(b).lower())
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
def _iter_list_elts(self, node: ast.AST):
|
|
264
|
+
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
265
|
+
for elt in node.elts:
|
|
266
|
+
yield elt
|
|
267
|
+
|
|
268
|
+
def _call_name_endswith(self, call: ast.Call, names):
|
|
269
|
+
if isinstance(call.func, ast.Name):
|
|
270
|
+
return call.func.id in names
|
|
271
|
+
if isinstance(call.func, ast.Attribute):
|
|
272
|
+
return call.func.attr in names
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
def _get_posarg(self, call: ast.Call, idx):
|
|
276
|
+
return call.args[idx] if len(call.args) > idx else None
|
|
277
|
+
|
|
278
|
+
def _simple_name(self, node: ast.AST):
|
|
279
|
+
if isinstance(node, ast.Name):
|
|
280
|
+
return node.id
|
|
281
|
+
if isinstance(node, ast.Attribute):
|
|
282
|
+
return node.attr
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def _mark_view_from_url_pattern(self, view_expr):
|
|
286
|
+
if view_expr is None:
|
|
287
|
+
return
|
|
288
|
+
if isinstance(view_expr, ast.Call) and isinstance(view_expr.func, ast.Attribute) and view_expr.func.attr == "as_view":
|
|
289
|
+
cls_name = self._simple_name(view_expr.func.value)
|
|
290
|
+
if cls_name:
|
|
291
|
+
self._mark_classes.add(cls_name)
|
|
292
|
+
self._mark_cbv_http_methods.add(cls_name)
|
|
293
|
+
else:
|
|
294
|
+
fname = self._simple_name(view_expr)
|
|
295
|
+
if fname:
|
|
296
|
+
self._mark_functions.add(fname)
|
|
297
|
+
|
|
298
|
+
def _scan_for_depends(self, node):
|
|
299
|
+
if not isinstance(node, ast.Call):
|
|
300
|
+
return
|
|
301
|
+
is_depends = False
|
|
302
|
+
if isinstance(node.func, ast.Name) and node.func.id == "Depends":
|
|
303
|
+
is_depends = True
|
|
304
|
+
elif isinstance(node.func, ast.Attribute) and node.func.attr == "Depends":
|
|
305
|
+
is_depends = True
|
|
306
|
+
if not is_depends:
|
|
307
|
+
return
|
|
308
|
+
if node.args:
|
|
309
|
+
dep = node.args[0]
|
|
310
|
+
dep_name = self._simple_name(dep)
|
|
311
|
+
if dep_name:
|
|
312
|
+
self._mark_functions.add(dep_name)
|
|
313
|
+
self.is_framework_file = True
|
|
314
|
+
|
|
315
|
+
def _collect_annotation_type_refs(self, fn: ast.FunctionDef):
|
|
316
|
+
def collect(t):
|
|
317
|
+
if t is None:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
if isinstance(t, ast.Name):
|
|
321
|
+
self._type_refs_in_routes.add(t.id)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if isinstance(t, ast.Attribute):
|
|
325
|
+
self._type_refs_in_routes.add(t.attr)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if isinstance(t, ast.Subscript):
|
|
329
|
+
collect(t.value)
|
|
330
|
+
slice_node = t.slice
|
|
331
|
+
if isinstance(slice_node, ast.Tuple):
|
|
332
|
+
for element in slice_node.elts:
|
|
333
|
+
collect(element)
|
|
334
|
+
else:
|
|
335
|
+
collect(slice_node)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if isinstance(t, ast.Tuple):
|
|
339
|
+
for element in t.elts:
|
|
340
|
+
collect(element)
|
|
341
|
+
|
|
342
|
+
all_args = []
|
|
343
|
+
all_args.extend(fn.args.args)
|
|
344
|
+
all_args.extend(fn.args.posonlyargs)
|
|
345
|
+
all_args.extend(fn.args.kwonlyargs)
|
|
346
|
+
|
|
347
|
+
for arg in all_args:
|
|
348
|
+
collect(arg.annotation)
|
|
349
|
+
|
|
350
|
+
if fn.returns:
|
|
351
|
+
collect(fn.returns)
|
|
352
|
+
|
|
353
|
+
def detect_framework_usage(definition, visitor=None):
|
|
145
354
|
if not visitor:
|
|
146
355
|
return None
|
|
147
|
-
|
|
148
|
-
# very low confidence - likely framework magic
|
|
149
356
|
if definition.line in visitor.framework_decorated_lines:
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
# framework file but no direct markers
|
|
153
|
-
if visitor.is_framework_file:
|
|
154
|
-
if (not definition.simple_name.startswith('_') and
|
|
155
|
-
definition.type in ('function', 'method')):
|
|
156
|
-
return 40
|
|
157
|
-
|
|
357
|
+
return 1
|
|
158
358
|
return None
|