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.
- diffcone/__init__.py +13 -0
- diffcone/cache.py +528 -0
- diffcone/check.py +399 -0
- diffcone/classify.py +255 -0
- diffcone/cli.py +886 -0
- diffcone/collect.py +846 -0
- diffcone/cython.py +757 -0
- diffcone/declarations.py +111 -0
- diffcone/discovery/__init__.py +121 -0
- diffcone/discovery/asv_static.py +494 -0
- diffcone/discovery/common.py +130 -0
- diffcone/discovery/pytest_static.py +2680 -0
- diffcone/evidence.py +738 -0
- diffcone/evidence_plan.py +1487 -0
- diffcone/execution.py +1681 -0
- diffcone/indexer/__init__.py +48 -0
- diffcone/indexer/core.py +286 -0
- diffcone/indexer/definitions.py +292 -0
- diffcone/indexer/dynamics.py +406 -0
- diffcone/indexer/facts.py +373 -0
- diffcone/indexer/literals.py +388 -0
- diffcone/indexer/references.py +895 -0
- diffcone/indexer/resolver.py +1001 -0
- diffcone/indexer/scopes.py +276 -0
- diffcone/indexer/state.py +83 -0
- diffcone/indexer/symbols.py +441 -0
- diffcone/indexer/syntax.py +173 -0
- diffcone/manifest.py +166 -0
- diffcone/model.py +191 -0
- diffcone/planner.py +1453 -0
- diffcone/report.py +256 -0
- diffcone/selection.py +133 -0
- diffcone/snapshot.py +739 -0
- diffcone/testing.py +200 -0
- diffcone-0.1.0.dist-info/METADATA +133 -0
- diffcone-0.1.0.dist-info/RECORD +39 -0
- diffcone-0.1.0.dist-info/WHEEL +4 -0
- diffcone-0.1.0.dist-info/entry_points.txt +3 -0
- diffcone-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|