diffcone 0.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.
@@ -0,0 +1,373 @@
1
+ """Per-module facts the first pass records and the second pass reads, and
2
+ their JSON codec for the module cache."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass, field
9
+
10
+ from diffcone.indexer.scopes import ClassScope, ImportBinding, ModuleScope, Scope
11
+ from diffcone.indexer.syntax import _digest
12
+ from diffcone.model import Edge, ExternalReference, Symbol, UnresolvedReference
13
+
14
+
15
+ @dataclass
16
+ class _FuncParams:
17
+ positional: list[str] # including self/cls for bound methods
18
+ bound: bool
19
+ defaults: dict[str, tuple[str, ...] | None]
20
+ has_varargs: bool
21
+
22
+
23
+ @dataclass
24
+ class _CallSite:
25
+ positional: list[tuple[str, ...] | None] # candidates per positional argument
26
+ keywords: dict[str, tuple[str, ...] | None]
27
+ unbounded: bool # *args / **kwargs at the call site
28
+ receiver_bound: bool # ``obj.m(...)`` / ``self.m(...)``: self is implicit
29
+ # The symbol the call sits in: a by-name import is attributed to the
30
+ # caller that named the module, not to the helper that imports it.
31
+ caller: str = ""
32
+ # Per argument, the caller's own parameter it is, when it is exactly
33
+ # that: ``skip_if_no(name)`` passes its parameter to the importer, so the
34
+ # answer is one level further out.
35
+ positional_params: list[str | None] = field(default_factory=list)
36
+ keyword_params: dict[str, str | None] = field(default_factory=dict)
37
+
38
+ def param_for(self, param: str, info: _FuncParams) -> str | None:
39
+ """The caller's parameter passed for ``param`` here, if it is one."""
40
+ if self.unbounded:
41
+ return None
42
+ if param in self.keyword_params:
43
+ return self.keyword_params[param]
44
+ if param in info.positional:
45
+ index = info.positional.index(param)
46
+ if info.bound and self.receiver_bound:
47
+ index -= 1
48
+ if 0 <= index < len(self.positional_params):
49
+ return self.positional_params[index]
50
+ return None
51
+
52
+ # The class each argument is an instance of, where the argument says so
53
+ # (``C()`` or ``C``); None when it does not. Used to bound what a
54
+ # ``getattr`` on a parameter may read.
55
+ positional_classes: list[str | None] = field(default_factory=list)
56
+ keyword_classes: dict[str, str | None] = field(default_factory=dict)
57
+ # The function an argument came out of (``make()``, or a local assigned
58
+ # once from it): its return class answers once every module is indexed.
59
+ positional_sources: list[str | None] = field(default_factory=list)
60
+ keyword_sources: dict[str, str | None] = field(default_factory=dict)
61
+
62
+ def class_for(self, param: str, info: _FuncParams) -> str | None:
63
+ """The class of the argument passed for ``param`` here, if it says."""
64
+ if self.unbounded:
65
+ return None
66
+ if param in self.keyword_classes:
67
+ return self.keyword_classes[param]
68
+ if param in info.positional:
69
+ index = info.positional.index(param)
70
+ if info.bound and self.receiver_bound:
71
+ index -= 1
72
+ if 0 <= index < len(self.positional_classes):
73
+ return self.positional_classes[index]
74
+ return None
75
+
76
+ def value_for(self, param: str, info: _FuncParams) -> tuple[str, ...] | None:
77
+ if self.unbounded:
78
+ return None
79
+ if param in self.keywords:
80
+ return self.keywords[param]
81
+ if param in info.positional:
82
+ index = info.positional.index(param)
83
+ if info.bound and self.receiver_bound:
84
+ index -= 1
85
+ if 0 <= index < len(self.positional):
86
+ return self.positional[index]
87
+ return info.defaults.get(param)
88
+
89
+
90
+ @dataclass
91
+ class _ParamDynamic:
92
+ function: str
93
+ param: str # a parameter name, or an instance attribute name with self_class
94
+ kind: str # "getattr" | "import"
95
+ base: list[str] | None # receiver chain for getattr, when it is a name chain
96
+ scope: Scope
97
+ detail: str
98
+ self_class: str = "" # set when the name is ``self.<param>`` of this class
99
+
100
+
101
+ @dataclass
102
+ class _AttrWrite:
103
+ """A write of ``self.<attr>`` in a method of ``cls``. ``binding`` is what
104
+ an ``__init__`` assignment binds (``["param", name]``, ``["strings",
105
+ [...]]``, ``["symbol", id]``); None for any other write."""
106
+
107
+ cls: str
108
+ attr: str
109
+ method: str
110
+ binding: list | None
111
+
112
+
113
+ @dataclass
114
+ class _AttrRef:
115
+ """``self.<attr>[.rest]`` read in ``source`` where no class in the MRO
116
+ defines ``attr``: resolved to the bound symbols when there are some."""
117
+
118
+ source: str
119
+ cls: str
120
+ attr: str
121
+ rest: list[str]
122
+ chain: str
123
+
124
+
125
+ @dataclass
126
+ class _Output:
127
+ """Everything a resolution pass writes. Pass 2 runs per module against a
128
+ fresh instance so a module's contribution can be cached and merged; the
129
+ rest of the indexer writes to the global one backed by the index."""
130
+
131
+ edges: set[Edge] = field(default_factory=set)
132
+ unresolved: set[UnresolvedReference] = field(default_factory=set)
133
+ external: set[ExternalReference] = field(default_factory=set)
134
+ call_sites: dict[str, list[_CallSite]] = field(default_factory=lambda: defaultdict(list))
135
+ escapes: set[str] = field(default_factory=set)
136
+ func_params: dict[str, _FuncParams] = field(default_factory=dict)
137
+ # Per function: the classes its ``return`` statements yield, when every
138
+ # one of them yields a class. A factory is how an object reaches code
139
+ # that reads attributes off it by a name nothing resolves.
140
+ returns: dict[str, tuple[str, ...]] = field(default_factory=dict)
141
+ param_dynamics: list[_ParamDynamic] = field(default_factory=list)
142
+ attr_writes: list[_AttrWrite] = field(default_factory=list)
143
+ # (class id, attribute) pairs whose value cannot be bounded; the class is
144
+ # "" for a write through a receiver of unknown type, the attribute "*"
145
+ # for every attribute.
146
+ attr_unbound: set[tuple[str, str]] = field(default_factory=set)
147
+ attr_refs: list[_AttrRef] = field(default_factory=list)
148
+ reflection: set[tuple[str, str]] = field(default_factory=set)
149
+ class_attributes: dict[str, dict[str, str]] = field(default_factory=dict)
150
+ class_bases: dict[str, tuple[str, ...]] = field(default_factory=dict)
151
+ open_classes: set[str] = field(default_factory=set)
152
+ doc_decorated: set[str] = field(default_factory=set)
153
+ # (decorated symbol, decorator symbol or "", receiver symbol or ""): who
154
+ # may hold the decorated function afterwards (see Indexer._registrations).
155
+ decorations: set[tuple[str, str, str]] = field(default_factory=set)
156
+
157
+ def merge(self, other: _Output) -> None:
158
+ self.edges |= other.edges
159
+ self.unresolved |= other.unresolved
160
+ self.external |= other.external
161
+ for function, sites in other.call_sites.items():
162
+ self.call_sites[function].extend(sites)
163
+ self.escapes |= other.escapes
164
+ self.func_params.update(other.func_params)
165
+ self.returns.update(other.returns)
166
+ self.param_dynamics.extend(other.param_dynamics)
167
+ self.attr_writes.extend(other.attr_writes)
168
+ self.attr_unbound |= other.attr_unbound
169
+ self.attr_refs.extend(other.attr_refs)
170
+ self.reflection |= other.reflection
171
+ self.class_attributes.update(other.class_attributes)
172
+ self.class_bases.update(other.class_bases)
173
+ self.open_classes |= other.open_classes
174
+ self.doc_decorated |= other.doc_decorated
175
+ self.decorations |= other.decorations
176
+
177
+
178
+ def _tuples(value: list | None) -> tuple[str, ...] | None:
179
+ return None if value is None else tuple(value)
180
+
181
+
182
+ def _scope_to_dict(scope: Scope) -> dict:
183
+ """The part of a function scope that deferred parameter-dynamic expansion
184
+ resolves names against (module, imports, locals, self, aliases)."""
185
+ return {
186
+ "module": scope.module.name,
187
+ "local_imports": {k: [b.module, b.attr] for k, b in scope.local_imports.items()},
188
+ "locals": sorted(scope.locals),
189
+ "self_name": scope.self_name,
190
+ "self_class": scope.self_class,
191
+ "param_aliases": scope.param_aliases,
192
+ }
193
+
194
+
195
+ def _scope_from_dict(data: dict, scopes: dict[str, ModuleScope]) -> Scope:
196
+ return Scope(
197
+ module=scopes[data["module"]],
198
+ local_imports={k: ImportBinding(m, a) for k, (m, a) in data["local_imports"].items()},
199
+ locals=set(data["locals"]),
200
+ self_name=data["self_name"],
201
+ self_class=data["self_class"],
202
+ param_aliases=dict(data["param_aliases"]),
203
+ )
204
+
205
+
206
+ def _output_to_dict(out: _Output) -> dict:
207
+ return {
208
+ "edges": [[e.source, e.target, e.kind, e.detail] for e in sorted(out.edges)],
209
+ "unresolved": [[u.symbol, u.kind, u.name, u.detail] for u in sorted(out.unresolved)],
210
+ "external": [[x.symbol, x.module] for x in sorted(out.external)],
211
+ "call_sites": {
212
+ f: [
213
+ [
214
+ s.positional,
215
+ s.keywords,
216
+ s.unbounded,
217
+ s.receiver_bound,
218
+ s.positional_classes,
219
+ s.keyword_classes,
220
+ s.positional_sources,
221
+ s.keyword_sources,
222
+ s.caller,
223
+ s.positional_params,
224
+ s.keyword_params,
225
+ ]
226
+ for s in sites
227
+ ]
228
+ for f, sites in out.call_sites.items()
229
+ },
230
+ "escapes": sorted(out.escapes),
231
+ "reflection": sorted(list(r) for r in out.reflection),
232
+ "class_bases": {c: list(b) for c, b in sorted(out.class_bases.items())},
233
+ "open_classes": sorted(out.open_classes),
234
+ "doc_decorated": sorted(out.doc_decorated),
235
+ "decorations": sorted(list(d) for d in out.decorations),
236
+ "class_attributes": {
237
+ c: dict(sorted(a.items())) for c, a in sorted(out.class_attributes.items())
238
+ },
239
+ "returns": {f: sorted(c) for f, c in sorted(out.returns.items())},
240
+ "func_params": {
241
+ f: [p.positional, p.bound, p.defaults, p.has_varargs]
242
+ for f, p in out.func_params.items()
243
+ },
244
+ "param_dynamics": [
245
+ [
246
+ pd.function,
247
+ pd.param,
248
+ pd.kind,
249
+ pd.base,
250
+ _scope_to_dict(pd.scope),
251
+ pd.detail,
252
+ pd.self_class,
253
+ ]
254
+ for pd in out.param_dynamics
255
+ ],
256
+ "attr_writes": [[w.cls, w.attr, w.method, w.binding] for w in out.attr_writes],
257
+ "attr_unbound": sorted(out.attr_unbound),
258
+ "attr_refs": [[r.source, r.cls, r.attr, r.rest, r.chain] for r in out.attr_refs],
259
+ }
260
+
261
+
262
+ def _output_from_dict(data: dict, scopes: dict[str, ModuleScope]) -> _Output:
263
+ out = _Output()
264
+ out.edges = {Edge(*e) for e in data["edges"]}
265
+ out.unresolved = {UnresolvedReference(*u) for u in data["unresolved"]}
266
+ out.external = {ExternalReference(*x) for x in data["external"]}
267
+ for f, sites in data["call_sites"].items():
268
+ out.call_sites[f] = [
269
+ _CallSite(
270
+ positional=[_tuples(v) for v in positional],
271
+ keywords={k: _tuples(v) for k, v in keywords.items()},
272
+ unbounded=unbounded,
273
+ receiver_bound=receiver_bound,
274
+ positional_classes=list(positional_classes),
275
+ keyword_classes=dict(keyword_classes),
276
+ positional_sources=list(positional_sources),
277
+ keyword_sources=dict(keyword_sources),
278
+ caller=caller,
279
+ positional_params=list(positional_params),
280
+ keyword_params=dict(keyword_params),
281
+ )
282
+ for (
283
+ positional,
284
+ keywords,
285
+ unbounded,
286
+ receiver_bound,
287
+ positional_classes,
288
+ keyword_classes,
289
+ positional_sources,
290
+ keyword_sources,
291
+ caller,
292
+ positional_params,
293
+ keyword_params,
294
+ ) in sites
295
+ ]
296
+ out.escapes = set(data["escapes"])
297
+ out.reflection = {(s, d) for s, d in data["reflection"]}
298
+ out.class_attributes = {c: dict(a) for c, a in data["class_attributes"].items()}
299
+ out.class_bases = {c: tuple(b) for c, b in data["class_bases"].items()}
300
+ out.open_classes = set(data["open_classes"])
301
+ out.doc_decorated = set(data["doc_decorated"])
302
+ out.decorations = {(a, b, c) for a, b, c in data["decorations"]}
303
+ out.returns = {f: tuple(c) for f, c in data["returns"].items()}
304
+ out.func_params = {
305
+ f: _FuncParams(
306
+ positional=list(positional),
307
+ bound=bound,
308
+ defaults={k: _tuples(v) for k, v in defaults.items()},
309
+ has_varargs=has_varargs,
310
+ )
311
+ for f, (positional, bound, defaults, has_varargs) in data["func_params"].items()
312
+ }
313
+ out.param_dynamics = [
314
+ _ParamDynamic(
315
+ function, param, kind, base, _scope_from_dict(scope, scopes), detail, self_class
316
+ )
317
+ for function, param, kind, base, scope, detail, self_class in data["param_dynamics"]
318
+ ]
319
+ out.attr_writes = [_AttrWrite(*w) for w in data["attr_writes"]]
320
+ out.attr_unbound = {(c, a) for c, a in data["attr_unbound"]}
321
+ out.attr_refs = [_AttrRef(*r) for r in data["attr_refs"]]
322
+ return out
323
+
324
+
325
+ def _facts_to_dict(
326
+ scope: ModuleScope, symbols: list[Symbol], classes: list[ClassScope], edges: set[Edge]
327
+ ) -> dict:
328
+ """A module's first-pass output: a pure function of its file (given its
329
+ module name), so it is cached by content. ``env`` digests the part other
330
+ modules' resolution can observe (names, kinds, class members); it feeds
331
+ the environment fingerprint that keys second-pass outputs."""
332
+ record = {
333
+ "imports": {k: [b.module, b.attr] for k, b in scope.imports.items()},
334
+ "alt_imports": {
335
+ k: [[b.module, b.attr] for b in v] for k, v in sorted(scope.alt_imports.items())
336
+ },
337
+ "star_imports": list(scope.star_imports),
338
+ "bindings": sorted(scope.bindings),
339
+ "members": dict(scope.members),
340
+ "variables": dict(scope.variables),
341
+ "literal_names": {
342
+ k: (list(v) if v is not None else None) for k, v in scope.literal_names.items()
343
+ },
344
+ "mutations": sorted(scope.mutations),
345
+ "symbols": [dict(vars(s)) for s in symbols], # flat and frozen: no deep copy needed
346
+ "classes": [
347
+ {
348
+ "id": c.id,
349
+ "enclosing": c.enclosing.id if c.enclosing is not None else None,
350
+ "members": dict(c.members),
351
+ "bindings": sorted(c.bindings),
352
+ "base_chains": c.base_chains,
353
+ "base_names": c.base_names,
354
+ "plain": c.plain,
355
+ }
356
+ for c in classes
357
+ ],
358
+ "edges": [[e.source, e.target, e.kind, e.detail] for e in sorted(edges)],
359
+ }
360
+ env = [
361
+ scope.name,
362
+ record["imports"],
363
+ record["alt_imports"],
364
+ record["star_imports"],
365
+ record["bindings"],
366
+ record["members"],
367
+ record["variables"],
368
+ record["mutations"],
369
+ [[s.id, s.kind, s.module, s.reads_docstrings] for s in symbols],
370
+ [[c["id"], c["enclosing"], c["members"], c["bindings"]] for c in record["classes"]],
371
+ ]
372
+ record["env"] = _digest(json.dumps(env, sort_keys=True))
373
+ return record