skylos 2.0.0__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 +48 -29
- skylos/cli.py +26 -79
- skylos/codemods.py +89 -0
- skylos/framework_aware.py +288 -94
- skylos/visitor.py +187 -42
- {skylos-2.0.0.dist-info → skylos-2.1.0.dist-info}/METADATA +4 -1
- {skylos-2.0.0.dist-info → skylos-2.1.0.dist-info}/RECORD +13 -11
- test/test_codemods.py +153 -0
- test/test_framework_aware.py +176 -242
- {skylos-2.0.0.dist-info → skylos-2.1.0.dist-info}/WHEEL +0 -0
- {skylos-2.0.0.dist-info → skylos-2.1.0.dist-info}/entry_points.txt +0 -0
- {skylos-2.0.0.dist-info → skylos-2.1.0.dist-info}/top_level.txt +0 -0
skylos/framework_aware.py
CHANGED
|
@@ -3,63 +3,45 @@ import fnmatch
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
FRAMEWORK_DECORATORS = [
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"@router.get", "@router.post", "@router.put", "@router.delete",
|
|
13
|
-
"@router.patch", "@router.head", "@router.options",
|
|
14
|
-
"@router.trace", "@router.websocket",
|
|
15
|
-
|
|
16
|
-
# django
|
|
17
|
-
"@*_required", "@login_required", "@permission_required",
|
|
18
|
-
"@user_passes_test", "@staff_member_required",
|
|
19
|
-
|
|
20
|
-
# 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",
|
|
21
12
|
"@validator", "@field_validator", "@model_validator", "@root_validator",
|
|
22
|
-
|
|
23
|
-
# celery patterns
|
|
24
|
-
"@task", "@shared_task", "@periodic_task", "@celery_task",
|
|
25
|
-
|
|
26
|
-
# generic ones
|
|
27
|
-
"@route", "@get", "@post", "@put", "@delete", "@patch",
|
|
28
|
-
"@middleware", "@depends", "@inject"
|
|
13
|
+
"@field_serializer", "@model_serializer", "@computed_field",
|
|
29
14
|
]
|
|
30
15
|
|
|
31
16
|
FRAMEWORK_FUNCTIONS = [
|
|
32
|
-
# django view methods
|
|
33
17
|
"get", "post", "put", "patch", "delete", "head", "options", "trace",
|
|
34
18
|
"*_queryset", "get_queryset", "get_object", "get_context_data",
|
|
35
19
|
"*_form", "form_valid", "form_invalid", "get_form_*",
|
|
36
|
-
|
|
37
|
-
# django model methods
|
|
38
|
-
"save", "delete", "clean", "full_clean", "*_delete", "*_save",
|
|
39
|
-
|
|
40
|
-
# generic API endpoints
|
|
41
|
-
"create_*", "update_*", "delete_*", "list_*", "retrieve_*",
|
|
42
|
-
"handle_*", "process_*", "*_handler", "*_view"
|
|
43
20
|
]
|
|
44
21
|
|
|
45
22
|
FRAMEWORK_IMPORTS = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'falcon', 'hug', 'responder', 'quart', 'hypercorn', 'uvicorn'
|
|
23
|
+
"flask", "fastapi", "django", "rest_framework", "pydantic", "celery",
|
|
24
|
+
"starlette", "uvicorn"
|
|
49
25
|
}
|
|
50
26
|
|
|
51
27
|
class FrameworkAwareVisitor:
|
|
52
|
-
|
|
53
|
-
def __init__(self, filename=None):
|
|
28
|
+
def __init__(self, filename = None):
|
|
54
29
|
self.is_framework_file = False
|
|
55
|
-
self.framework_decorated_lines = set()
|
|
56
30
|
self.detected_frameworks = set()
|
|
57
|
-
|
|
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()
|
|
58
40
|
if filename:
|
|
59
41
|
self._check_framework_imports_in_file(filename)
|
|
60
42
|
|
|
61
43
|
def visit(self, node):
|
|
62
|
-
method =
|
|
44
|
+
method = "visit_" + node.__class__.__name__
|
|
63
45
|
visitor = getattr(self, method, self.generic_visit)
|
|
64
46
|
return visitor(node)
|
|
65
47
|
|
|
@@ -72,93 +54,305 @@ class FrameworkAwareVisitor:
|
|
|
72
54
|
elif isinstance(value, ast.AST):
|
|
73
55
|
self.visit(value)
|
|
74
56
|
|
|
75
|
-
def visit_Import(self, node):
|
|
57
|
+
def visit_Import(self, node: ast.Import):
|
|
76
58
|
for alias in node.names:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
|
|
80
68
|
self.generic_visit(node)
|
|
81
69
|
|
|
82
|
-
def visit_ImportFrom(self, node):
|
|
70
|
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
83
71
|
if node.module:
|
|
84
|
-
module_name = node.module.split(
|
|
85
|
-
|
|
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
|
-
|
|
90
76
|
self.generic_visit(node)
|
|
91
77
|
|
|
92
|
-
def visit_FunctionDef(self, node):
|
|
78
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
79
|
+
self.func_defs.setdefault(node.name, node.lineno)
|
|
93
80
|
for deco in node.decorator_list:
|
|
94
|
-
|
|
81
|
+
d = self._normalize_decorator(deco)
|
|
95
82
|
|
|
96
|
-
if self._matches_framework_pattern(
|
|
83
|
+
if self._matches_framework_pattern(d, FRAMEWORK_DECORATORS):
|
|
97
84
|
self.framework_decorated_lines.add(node.lineno)
|
|
98
85
|
self.is_framework_file = True
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if self.is_framework_file:
|
|
86
|
+
|
|
87
|
+
if self._decorator_base_name_is(deco, "receiver"):
|
|
102
88
|
self.framework_decorated_lines.add(node.lineno)
|
|
103
|
-
|
|
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)
|
|
104
106
|
self.generic_visit(node)
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
self.visit_FunctionDef(node)
|
|
108
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
108
109
|
|
|
109
|
-
def visit_ClassDef(self, node):
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
116
138
|
self.generic_visit(node)
|
|
117
139
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return "@unknown"
|
|
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)
|
|
133
153
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
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)
|
|
137
194
|
|
|
138
195
|
def _check_framework_imports_in_file(self, filename):
|
|
139
196
|
try:
|
|
140
|
-
content = Path(filename).read_text(encoding=
|
|
197
|
+
content = Path(filename).read_text(encoding="utf-8")
|
|
198
|
+
|
|
141
199
|
for framework in FRAMEWORK_IMPORTS:
|
|
142
|
-
|
|
143
|
-
|
|
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:
|
|
144
207
|
self.is_framework_file = True
|
|
145
208
|
self.detected_frameworks.add(framework)
|
|
146
209
|
break
|
|
147
|
-
|
|
210
|
+
|
|
211
|
+
except Exception:
|
|
148
212
|
pass
|
|
149
213
|
|
|
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
|
+
|
|
150
353
|
def detect_framework_usage(definition, visitor=None):
|
|
151
354
|
if not visitor:
|
|
152
355
|
return None
|
|
153
|
-
|
|
154
|
-
# very low confidence - likely framework magic
|
|
155
356
|
if definition.line in visitor.framework_decorated_lines:
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
# framework file but no direct markers
|
|
159
|
-
if visitor.is_framework_file:
|
|
160
|
-
if (not definition.simple_name.startswith('_') and
|
|
161
|
-
definition.type in ('function', 'method')):
|
|
162
|
-
return 40
|
|
163
|
-
|
|
357
|
+
return 1
|
|
164
358
|
return None
|