skylos 2.0.0__py3-none-any.whl → 2.1.1__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
@@ -3,63 +3,45 @@ import fnmatch
3
3
  from pathlib import Path
4
4
 
5
5
  FRAMEWORK_DECORATORS = [
6
- # flask
7
- "@app.route", "@app.get", "@app.post", "@app.put", "@app.delete",
8
- "@app.patch", "@app.before_request", "@app.after_request",
9
- "@app.errorhandler", "@app.teardown_*",
10
-
11
- # fastapi
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
- 'flask', 'fastapi', 'django', 'pydantic', 'celery', 'starlette',
47
- 'sanic', 'tornado', 'pyramid', 'bottle', 'cherrypy', 'web2py',
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 = 'visit_' + node.__class__.__name__
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
- if any(fw in alias.name.lower() for fw in FRAMEWORK_IMPORTS):
78
- self.is_framework_file = True
79
- 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
+
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('.')[0].lower()
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
- decorator_str = self._normalize_decorator(deco)
81
+ d = self._normalize_decorator(deco)
95
82
 
96
- if self._matches_framework_pattern(decorator_str, FRAMEWORK_DECORATORS):
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
- if self._matches_framework_pattern(node.name, FRAMEWORK_FUNCTIONS):
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
- def visit_AsyncFunctionDef(self, node):
107
- self.visit_FunctionDef(node)
108
+ visit_AsyncFunctionDef = visit_FunctionDef
108
109
 
109
- def visit_ClassDef(self, node):
110
- for base in node.bases:
111
- if isinstance(base, ast.Name):
112
- if any(pattern in base.id.lower() for pattern in ['view', 'viewset', 'api', 'handler']):
113
- if self.is_framework_file:
114
- self.framework_decorated_lines.add(node.lineno)
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 _normalize_decorator(self, decorator_node):
119
- if isinstance(decorator_node, ast.Name):
120
- return f"@{decorator_node.id}"
121
-
122
- elif isinstance(decorator_node, ast.Attribute):
123
- if isinstance(decorator_node.value, ast.Name):
124
- return f"@{decorator_node.value.id}.{decorator_node.attr}"
125
-
126
- else:
127
- return f"@{decorator_node.attr}"
128
-
129
- elif isinstance(decorator_node, ast.Call):
130
- return self._normalize_decorator(decorator_node.func)
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 _matches_framework_pattern(self, text, patterns):
135
- text_clean = text.lstrip('@')
136
- 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)
137
194
 
138
195
  def _check_framework_imports_in_file(self, filename):
139
196
  try:
140
- content = Path(filename).read_text(encoding='utf-8')
197
+ content = Path(filename).read_text(encoding="utf-8")
198
+
141
199
  for framework in FRAMEWORK_IMPORTS:
142
- if (f'import {framework}' in content or
143
- 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:
144
207
  self.is_framework_file = True
145
208
  self.detected_frameworks.add(framework)
146
209
  break
147
- except:
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 20
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