stackthread 0.1.0.dev2__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 stackthread might be problematic. Click here for more details.

Files changed (40) hide show
  1. stackthread/__init__.py +6 -0
  2. stackthread/__main__.py +3 -0
  3. stackthread/_vendor/THIRD_PARTY_NOTICES.txt +23 -0
  4. stackthread/_vendor/extract.mjs +15 -0
  5. stackthread/adapters.py +130 -0
  6. stackthread/cli.py +342 -0
  7. stackthread/config.py +409 -0
  8. stackthread/detect.py +150 -0
  9. stackthread/lang/__init__.py +1 -0
  10. stackthread/lang/js/__init__.py +1 -0
  11. stackthread/lang/js/aliases.py +133 -0
  12. stackthread/lang/js/build.py +455 -0
  13. stackthread/lang/js/worker.py +186 -0
  14. stackthread/lang/python/__init__.py +1 -0
  15. stackthread/lang/python/build.py +249 -0
  16. stackthread/lang/python/extract.py +494 -0
  17. stackthread/lang/python/frameworks.py +700 -0
  18. stackthread/lang/python/orm.py +300 -0
  19. stackthread/lang/python/resolve.py +448 -0
  20. stackthread/lineage.py +965 -0
  21. stackthread/link.py +130 -0
  22. stackthread/logs.py +314 -0
  23. stackthread/model.py +161 -0
  24. stackthread/paths.py +173 -0
  25. stackthread/project.py +414 -0
  26. stackthread/report.py +203 -0
  27. stackthread/rules.py +48 -0
  28. stackthread/server/__init__.py +5 -0
  29. stackthread/server/app.py +696 -0
  30. stackthread/server/edit.py +108 -0
  31. stackthread/server/security.py +73 -0
  32. stackthread/static/THIRD_PARTY_NOTICES.txt +379 -0
  33. stackthread/static/app.css +1 -0
  34. stackthread/static/app.js +29 -0
  35. stackthread/static/index.html +16 -0
  36. stackthread-0.1.0.dev2.dist-info/METADATA +51 -0
  37. stackthread-0.1.0.dev2.dist-info/RECORD +40 -0
  38. stackthread-0.1.0.dev2.dist-info/WHEEL +4 -0
  39. stackthread-0.1.0.dev2.dist-info/entry_points.txt +2 -0
  40. stackthread-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,448 @@
1
+ """Name resolution across a Python source (design §8.3).
2
+
3
+ Works only from facts, so it is fast and testable. Resolution follows imports and re-exports through packages,
4
+ `self`/`cls`/`super()`, class bases inside the project, and simple local type hints (`x = Foo()`, `db: Session`,
5
+ `with Session() as s`, `x = Order.objects.get(...)`).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from collections import defaultdict
12
+ from dataclasses import dataclass
13
+ from typing import Any, Literal
14
+
15
+ RefKind = Literal["def", "module", "var", "external"]
16
+
17
+ #: Callee names too common to resolve by name alone; they stay unresolved unless the receiver's type is known.
18
+ COMMON_METHODS = frozenset(
19
+ {
20
+ "get",
21
+ "set",
22
+ "update",
23
+ "save",
24
+ "delete",
25
+ "add",
26
+ "append",
27
+ "extend",
28
+ "pop",
29
+ "remove",
30
+ "items",
31
+ "keys",
32
+ "values",
33
+ "copy",
34
+ "format",
35
+ "join",
36
+ "split",
37
+ "strip",
38
+ "replace",
39
+ "filter",
40
+ "all",
41
+ "first",
42
+ "last",
43
+ "count",
44
+ "create",
45
+ "exists",
46
+ "order_by",
47
+ "select_related",
48
+ "prefetch_related",
49
+ "annotate",
50
+ "aggregate",
51
+ "values_list",
52
+ "exclude",
53
+ "distinct",
54
+ "only",
55
+ "defer",
56
+ "read",
57
+ "write",
58
+ "close",
59
+ "open",
60
+ "send",
61
+ "run",
62
+ "start",
63
+ "stop",
64
+ "call",
65
+ "apply",
66
+ "execute",
67
+ "commit",
68
+ "flush",
69
+ "query",
70
+ "load",
71
+ "dump",
72
+ "dumps",
73
+ "loads",
74
+ "encode",
75
+ "decode",
76
+ "lower",
77
+ "upper",
78
+ "startswith",
79
+ "endswith",
80
+ "setdefault",
81
+ "sort",
82
+ "reverse",
83
+ "clear",
84
+ "insert",
85
+ "index",
86
+ "find",
87
+ "match",
88
+ "search",
89
+ "sub",
90
+ "group",
91
+ "validate",
92
+ "clean",
93
+ "is_valid",
94
+ "render",
95
+ "dispatch",
96
+ "handle",
97
+ "process",
98
+ "parse",
99
+ "build",
100
+ "check",
101
+ "init",
102
+ "setup",
103
+ "reset",
104
+ "refresh",
105
+ "next",
106
+ "list",
107
+ "retrieve",
108
+ "destroy",
109
+ "partial_update",
110
+ "post",
111
+ "put",
112
+ "patch",
113
+ "head",
114
+ "options",
115
+ "info",
116
+ "debug",
117
+ "warning",
118
+ "error",
119
+ "exception",
120
+ "critical",
121
+ "log",
122
+ "emit",
123
+ "wait",
124
+ "result",
125
+ "submit",
126
+ "map",
127
+ "__init__",
128
+ "__call__",
129
+ "__enter__",
130
+ "__exit__",
131
+ "__str__",
132
+ "__repr__",
133
+ }
134
+ )
135
+
136
+ _SEGMENT = re.compile(r"[^.\[\]()]+(?:\(\))?(?:\[[^\]]*\])*")
137
+
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class Ref:
141
+ kind: RefKind
142
+ key: str # def: "module:qualname"; module: dotted name; var: "module:name"; external: dotted path
143
+
144
+ @property
145
+ def module(self) -> str:
146
+ return self.key.split(":", 1)[0] if self.kind in ("def", "var") else self.key
147
+
148
+
149
+ def split_dotted(path: str) -> list[str]:
150
+ """Split a callee path into segments.
151
+
152
+ `Order.objects.filter().update` -> [Order, objects, filter(), update]; `db[orders].find` -> [db[orders], find].
153
+ """
154
+ return _SEGMENT.findall(path)
155
+
156
+
157
+ def bare(segment: str) -> str:
158
+ return segment.split("(", 1)[0].split("[", 1)[0]
159
+
160
+
161
+ class PyIndex:
162
+ """Every module of one Python source, with symbol tables and lookups."""
163
+
164
+ def __init__(self, facts: list[dict[str, Any]]) -> None:
165
+ self.modules: dict[str, dict[str, Any]] = {}
166
+ self.defs: dict[str, dict[str, Any]] = {}
167
+ self.symbols: dict[str, dict[str, tuple[str, Any]]] = defaultdict(dict)
168
+ self.stars: dict[str, list[str]] = defaultdict(list)
169
+ self.vars: dict[str, dict[str, Any]] = {}
170
+ self.methods_by_name: dict[str, list[str]] = defaultdict(list)
171
+ self.children: dict[str, dict[str, str]] = defaultdict(dict)
172
+ self._resolving: set[str] = set()
173
+ for f in facts:
174
+ if f["module"]:
175
+ self.modules[f["module"]] = f
176
+ for module, f in self.modules.items():
177
+ table = self.symbols[module]
178
+ for imp in f["imports"]:
179
+ if imp["name"] == "*":
180
+ self.stars[module].append(imp["module"])
181
+ elif not imp.get("lazy") or imp["local"] not in table:
182
+ table[imp["local"]] = ("import", imp)
183
+ for a in f["assigns"]:
184
+ key = f"{module}:{a['name']}"
185
+ if a["aug"] and key in self.vars:
186
+ self.vars[key].setdefault("augments", []).append(a)
187
+ continue
188
+ self.vars[key] = dict(a, module=module)
189
+ table[a["name"]] = ("var", key)
190
+ for d in f["defs"]:
191
+ key = f"{module}:{d['q']}"
192
+ d = dict(d, module=module, file=f["file"], key=key)
193
+ self.defs[key] = d
194
+ if d["parent"] is None:
195
+ table[d["name"]] = ("def", key)
196
+ else:
197
+ self.children[f"{module}:{d['parent']}"][d["name"]] = key
198
+ if d["kind"] == "method" and not d["name"].startswith("__"):
199
+ self.methods_by_name[d["name"]].append(key)
200
+
201
+ # ---- names ----
202
+
203
+ def resolve_name(self, module: str, name: str, _seen: frozenset[tuple[str, str]] = frozenset()) -> Ref | None:
204
+ if (module, name) in _seen:
205
+ return None
206
+ seen = _seen | {(module, name)}
207
+ entry = self.symbols.get(module, {}).get(name)
208
+ if entry is None:
209
+ if f"{module}.{name}" in self.modules: # a submodule reached as a package attribute
210
+ return Ref("module", f"{module}.{name}")
211
+ for star in self.stars.get(module, []):
212
+ if star in self.modules and (found := self.resolve_name(star, name, seen)):
213
+ return found
214
+ return None
215
+ kind, data = entry
216
+ if kind == "def":
217
+ return Ref("def", data)
218
+ if kind == "var":
219
+ return Ref("var", data)
220
+ imp = data
221
+ target, imported = imp["module"], imp["name"]
222
+ if imported is None:
223
+ return Ref("module", target) if target in self.modules else Ref("external", target)
224
+ if f"{target}.{imported}" in self.modules:
225
+ return Ref("module", f"{target}.{imported}")
226
+ if target in self.modules:
227
+ return self.resolve_name(target, imported, seen) or Ref("external", f"{target}.{imported}")
228
+ return Ref("external", f"{target}.{imported}" if target else imported)
229
+
230
+ def resolve_dotted(self, module: str, path: str, scope: dict[str, Any] | None = None) -> Ref | None:
231
+ """Resolve `a.b.c` from inside `module` (and optionally a function scope with local type hints)."""
232
+ segments = split_dotted(path)
233
+ if not segments:
234
+ return None
235
+ head = bare(segments[0])
236
+ ref: Ref | None = None
237
+ if scope is not None:
238
+ ref = self._scope_head(module, head, scope)
239
+ if ref is None:
240
+ ref = self.resolve_name(module, head)
241
+ if ref is None:
242
+ return None
243
+ for seg in segments[1:]:
244
+ ref = self.member(ref, bare(seg))
245
+ if ref is None:
246
+ return None
247
+ return ref
248
+
249
+ def _scope_head(self, module: str, head: str, scope: dict[str, Any]) -> Ref | None:
250
+ if head in ("self", "cls"):
251
+ cls = self.enclosing_class(scope)
252
+ return Ref("def", cls["key"]) if cls else None
253
+ hint = self.local_hint(scope, head)
254
+ if hint is None:
255
+ return None
256
+ return self.instance_type(module, hint, scope)
257
+
258
+ def local_hint(self, scope: dict[str, Any], name: str) -> str | None:
259
+ current: dict[str, Any] | None = scope
260
+ while current is not None:
261
+ if name in current.get("locals", {}):
262
+ return str(current["locals"][name])
263
+ parent = current.get("parent")
264
+ current = self.defs.get(f"{current['module']}:{parent}") if parent else None
265
+ return None
266
+
267
+ def instance_type(self, module: str, hint: str, scope: dict[str, Any] | None = None) -> Ref | None:
268
+ """What an instance described by a hint is: `Foo()` -> class Foo; `Session` -> class Session;
269
+ `make_service()` -> its return annotation; `Order.objects.get()` -> model Order."""
270
+ orm = re.match(r"^([\w.]+)\.(?:objects|_default_manager|_base_manager)\.(\w+)\(\)(\[\])?$", hint)
271
+ if orm:
272
+ model = self.resolve_dotted(module, orm.group(1))
273
+ if model and model.kind == "def" and (orm.group(2) in INSTANCE_OPS or orm.group(3)):
274
+ return model
275
+ return None
276
+ called = hint.endswith("()")
277
+ target = hint[:-2] if called else hint
278
+ if target.endswith("()[]") or "(" in target:
279
+ return None
280
+ ref = self.resolve_dotted(module, target) # module scope only: local hints never refer to locals
281
+ if ref is None or ref.kind != "def":
282
+ return ref if ref and ref.kind == "external" else None
283
+ d = self.defs[ref.key]
284
+ if d["kind"] == "class":
285
+ return ref
286
+ if called and d.get("returns"):
287
+ ret = self.resolve_dotted(d["module"], d["returns"][0])
288
+ if ret and ret.kind == "def" and self.defs[ret.key]["kind"] == "class":
289
+ return ret
290
+ return None
291
+
292
+ def member(self, ref: Ref, name: str) -> Ref | None:
293
+ if ref.kind == "module":
294
+ return self.resolve_name(ref.key, name)
295
+ if ref.kind == "external":
296
+ return Ref("external", f"{ref.key}.{name}")
297
+ if ref.kind == "var":
298
+ if ref.key in self._resolving: # `app = app.with_options()` and similar self-references
299
+ return None
300
+ v = self.vars.get(ref.key)
301
+ value = v and v.get("value")
302
+ if value and value.get("t") == "call":
303
+ self._resolving.add(ref.key)
304
+ try:
305
+ inst = self.instance_type(ref.module, f"{value['f']}()")
306
+ finally:
307
+ self._resolving.discard(ref.key)
308
+ if inst is not None:
309
+ return self.member(inst, name)
310
+ return None
311
+ d = self.defs.get(ref.key)
312
+ if d is None:
313
+ return None
314
+ if d["kind"] == "class":
315
+ found = self.find_in_class(ref.key, name)
316
+ if found:
317
+ return found
318
+ attr_type = self.class_attr_type(ref.key, name)
319
+ return attr_type
320
+ return None
321
+
322
+ # ---- classes ----
323
+
324
+ def enclosing_class(self, scope: dict[str, Any]) -> dict[str, Any] | None:
325
+ current: dict[str, Any] | None = scope
326
+ while current is not None:
327
+ if current["kind"] == "class":
328
+ return current
329
+ parent = current.get("parent")
330
+ current = self.defs.get(f"{current['module']}:{parent}") if parent else None
331
+ return None
332
+
333
+ def bases(self, class_key: str) -> list[Ref]:
334
+ d = self.defs.get(class_key)
335
+ if d is None:
336
+ return []
337
+ out = []
338
+ for base in d.get("bases", []):
339
+ ref = self.resolve_dotted(d["module"], base)
340
+ out.append(ref if ref else Ref("external", base))
341
+ return out
342
+
343
+ def mro(self, class_key: str) -> list[str]:
344
+ """The class and its project bases, depth first, without repeats."""
345
+ out: list[str] = []
346
+ stack = [class_key]
347
+ while stack:
348
+ key = stack.pop(0)
349
+ if key in out or key not in self.defs:
350
+ continue
351
+ out.append(key)
352
+ stack[0:0] = [b.key for b in self.bases(key) if b.kind == "def"]
353
+ return out
354
+
355
+ def external_bases(self, class_key: str) -> list[str]:
356
+ names: list[str] = []
357
+ for key in self.mro(class_key):
358
+ names += [b.key for b in self.bases(key) if b.kind == "external"]
359
+ return names
360
+
361
+ def find_in_class(self, class_key: str, name: str) -> Ref | None:
362
+ for key in self.mro(class_key):
363
+ child = self.children.get(key, {}).get(name)
364
+ if child:
365
+ return Ref("def", child)
366
+ return None
367
+
368
+ def class_attr_type(self, class_key: str, name: str) -> Ref | None:
369
+ for key in self.mro(class_key):
370
+ d = self.defs[key]
371
+ hint = d.get("self_attrs", {}).get(name)
372
+ if hint:
373
+ return self.instance_type(d["module"], hint)
374
+ return None
375
+
376
+ # ---- calls ----
377
+
378
+ def resolve_call(self, scope: dict[str, Any], callee: str) -> tuple[Ref | None, str]:
379
+ """Resolve a call made inside `scope`. Returns the target and a confidence word."""
380
+ module = scope["module"]
381
+ if callee.startswith("super()."):
382
+ cls = self.enclosing_class(scope)
383
+ name = bare(split_dotted(callee)[1]) if len(split_dotted(callee)) > 1 else ""
384
+ if cls and name:
385
+ for base in self.mro(cls["key"])[1:]:
386
+ child = self.children.get(base, {}).get(name)
387
+ if child:
388
+ return Ref("def", child), "exact"
389
+ return None, "exact"
390
+ segments = split_dotted(callee)
391
+ if not segments:
392
+ return None, "exact"
393
+ if any("(" in s for s in segments[:-1]): # a call on a call result: type unknown
394
+ return self._by_name(bare(segments[-1]))
395
+ ref = self.resolve_dotted(module, callee, scope)
396
+ if ref is not None:
397
+ return ref, "exact"
398
+ if len(segments) > 1:
399
+ return self._by_name(bare(segments[-1]))
400
+ return None, "exact"
401
+
402
+ def _by_name(self, name: str) -> tuple[Ref | None, str]:
403
+ if name in COMMON_METHODS or name.startswith("_"):
404
+ return None, "exact"
405
+ candidates = self.methods_by_name.get(name, [])
406
+ if len(candidates) == 1:
407
+ return Ref("def", candidates[0]), "inferred"
408
+ return None, "exact"
409
+
410
+ def const(self, module: str, value: dict[str, Any] | None) -> Any:
411
+ """Evaluate a literal-ish value: literals, names of module constants (followed through imports), lists,
412
+ concatenations of strings."""
413
+ if value is None:
414
+ return None
415
+ t = value.get("t")
416
+ if t == "lit":
417
+ return value["v"]
418
+ if t == "list":
419
+ return [self.const(module, v) for v in value["v"]]
420
+ if t == "concat":
421
+ parts = [self.const(module, v) for v in value["v"]]
422
+ return "".join(parts) if all(isinstance(p, str) for p in parts) else None
423
+ if t == "name":
424
+ ref = self.resolve_dotted(module, value["v"])
425
+ if ref and ref.kind == "var":
426
+ var = self.vars.get(ref.key)
427
+ if var and var.get("value") and var["value"].get("t") in ("lit", "concat", "list"):
428
+ return self.const(ref.module, var["value"])
429
+ return None
430
+
431
+
432
+ #: Django manager calls that return a model instance rather than a queryset.
433
+ INSTANCE_OPS = frozenset(
434
+ {
435
+ "get",
436
+ "create",
437
+ "first",
438
+ "last",
439
+ "latest",
440
+ "earliest",
441
+ "get_or_create",
442
+ "update_or_create",
443
+ "aget",
444
+ "acreate",
445
+ "afirst",
446
+ "alast",
447
+ }
448
+ )