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/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
- # flask
8
- "@app.route", "@app.get", "@app.post", "@app.put", "@app.delete",
9
- "@app.patch", "@app.before_request", "@app.after_request",
10
- "@app.errorhandler", "@app.teardown_*",
11
-
12
- # fastapi
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
- 'flask', 'fastapi', 'django', 'pydantic', 'celery', 'starlette',
48
- 'sanic', 'tornado', 'pyramid', 'bottle', 'cherrypy', 'web2py',
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 = 'visit_' + node.__class__.__name__
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
- if any(fw in alias.name.lower() for fw in FRAMEWORK_IMPORTS):
79
- self.is_framework_file = True
80
- self.detected_frameworks.add(alias.name.split('.')[0])
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('.')[0].lower()
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
- decorator_str = self._normalize_decorator(deco)
94
- if self._matches_framework_pattern(decorator_str, FRAMEWORK_DECORATORS):
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
- if self._matches_framework_pattern(node.name, FRAMEWORK_FUNCTIONS):
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
- def visit_AsyncFunctionDef(self, node):
105
- self.visit_FunctionDef(node)
108
+ visit_AsyncFunctionDef = visit_FunctionDef
106
109
 
107
- def visit_ClassDef(self, node):
108
- for base in node.bases:
109
- if isinstance(base, ast.Name):
110
- if any(pattern in base.id.lower() for pattern in ['view', 'viewset', 'api', 'handler']):
111
- if self.is_framework_file:
112
- self.framework_decorated_lines.add(node.lineno)
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 _normalize_decorator(self, decorator_node):
117
- if isinstance(decorator_node, ast.Name):
118
- return f"@{decorator_node.id}"
119
- elif isinstance(decorator_node, ast.Attribute):
120
- if isinstance(decorator_node.value, ast.Name):
121
- return f"@{decorator_node.value.id}.{decorator_node.attr}"
122
- else:
123
- return f"@{decorator_node.attr}"
124
- elif isinstance(decorator_node, ast.Call):
125
- return self._normalize_decorator(decorator_node.func)
126
- 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)
127
153
 
128
- def _matches_framework_pattern(self, text, patterns):
129
- text_clean = text.lstrip('@')
130
- return any(fnmatch.fnmatch(text_clean, pattern.lstrip('@')) for pattern in patterns)
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='utf-8')
197
+ content = Path(filename).read_text(encoding="utf-8")
198
+
135
199
  for framework in FRAMEWORK_IMPORTS:
136
- if (f'import {framework}' in content or
137
- f'from {framework}' in content):
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
- except:
210
+
211
+ except Exception:
142
212
  pass
143
213
 
144
- def detect_framework_usage(definition, decorator_nodes=None, visitor=None):
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 20
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