offwork 0.4.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.
- offwork/__init__.py +167 -0
- offwork/__main__.py +770 -0
- offwork/_venv.py +174 -0
- offwork/core/__init__.py +15 -0
- offwork/core/errors.py +83 -0
- offwork/core/models.py +174 -0
- offwork/core/pairing.py +389 -0
- offwork/core/progress.py +91 -0
- offwork/core/signing.py +91 -0
- offwork/core/task.py +520 -0
- offwork/core/token.py +184 -0
- offwork/core/version.py +10 -0
- offwork/graph/__init__.py +5 -0
- offwork/graph/analyzer.py +637 -0
- offwork/graph/decorator.py +87 -0
- offwork/graph/graph.py +995 -0
- offwork/graph/store.py +500 -0
- offwork/graph/tracing.py +429 -0
- offwork/py.typed +0 -0
- offwork/typing.py +48 -0
- offwork/worker/__init__.py +18 -0
- offwork/worker/backends/__init__.py +3 -0
- offwork/worker/backends/base.py +149 -0
- offwork/worker/backends/http.py +237 -0
- offwork/worker/backends/local.py +452 -0
- offwork/worker/backends/rabbitmq.py +410 -0
- offwork/worker/backends/redis.py +175 -0
- offwork/worker/deps.py +365 -0
- offwork/worker/remote.py +793 -0
- offwork/worker/result.py +276 -0
- offwork/worker/sandbox/Dockerfile +24 -0
- offwork/worker/sandbox/__init__.py +18 -0
- offwork/worker/sandbox/_protocol.py +50 -0
- offwork/worker/sandbox/docker.py +438 -0
- offwork/worker/sandbox/guest_agent.py +622 -0
- offwork/worker/schedule.py +26 -0
- offwork/worker/worker.py +263 -0
- offwork-0.4.0.dist-info/METADATA +143 -0
- offwork-0.4.0.dist-info/RECORD +42 -0
- offwork-0.4.0.dist-info/WHEEL +4 -0
- offwork-0.4.0.dist-info/entry_points.txt +3 -0
- offwork-0.4.0.dist-info/licenses/LICENSE +661 -0
offwork/graph/store.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Content-addressable store for serializing and reconstructing functions."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
from graphlib import TopologicalSorter
|
|
8
|
+
from dataclasses import field, dataclass
|
|
9
|
+
|
|
10
|
+
from offwork.core.models import ImportInfo, FunctionNode
|
|
11
|
+
from offwork.core.version import _VERSION
|
|
12
|
+
from offwork.graph.analyzer import hoist_closure_vars, hoist_closure_func_refs
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# -- Reconstruction helpers (used by Store.reconstruct) ----------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _topological_order(nodes: dict[str, FunctionNode]) -> list[str]:
|
|
21
|
+
"""Return qualified names in dependency-first order."""
|
|
22
|
+
sorter: TopologicalSorter[str] = TopologicalSorter()
|
|
23
|
+
for qname, node in nodes.items():
|
|
24
|
+
sorter.add(qname, *[dep for dep in node.dependencies if dep in nodes])
|
|
25
|
+
return list(sorter.static_order())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _collect_imports(
|
|
29
|
+
nodes: dict[str, FunctionNode], order: list[str]
|
|
30
|
+
) -> list[str]:
|
|
31
|
+
"""Deduplicate and sort import statements across all nodes."""
|
|
32
|
+
seen: dict[str, None] = {}
|
|
33
|
+
for qname in order:
|
|
34
|
+
for imp in nodes[qname].imports:
|
|
35
|
+
seen.setdefault(imp.statement, None)
|
|
36
|
+
return sorted(seen)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _collect_module_vars(
|
|
40
|
+
nodes: dict[str, FunctionNode], order: list[str]
|
|
41
|
+
) -> list[str]:
|
|
42
|
+
"""Deduplicate module-level variable assignments across all nodes."""
|
|
43
|
+
seen: dict[str, str] = {}
|
|
44
|
+
func_and_class_names = {node.name for node in nodes.values()}
|
|
45
|
+
for qname in order:
|
|
46
|
+
for var_name, var_src in nodes[qname].module_vars.items():
|
|
47
|
+
if var_name not in seen and var_name not in func_and_class_names:
|
|
48
|
+
seen[var_name] = var_src
|
|
49
|
+
return list(seen.values())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _group_by_class(
|
|
53
|
+
nodes: dict[str, FunctionNode], order: list[str]
|
|
54
|
+
) -> dict[str, list[str]]:
|
|
55
|
+
"""Group qualified names by owner_class, preserving topological order."""
|
|
56
|
+
groups: dict[str, list[str]] = {}
|
|
57
|
+
for qname in order:
|
|
58
|
+
owner = nodes[qname].owner_class
|
|
59
|
+
if owner is not None:
|
|
60
|
+
groups.setdefault(owner, []).append(qname)
|
|
61
|
+
return groups
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _apply_closure_transforms(
|
|
65
|
+
node: FunctionNode, all_nodes: dict[str, FunctionNode]
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Apply closure variable and function reference hoisting to source."""
|
|
68
|
+
source = node.source
|
|
69
|
+
if node.closure_vars:
|
|
70
|
+
source = hoist_closure_vars(source, node.closure_vars)
|
|
71
|
+
if node.closure_func_refs:
|
|
72
|
+
source = hoist_closure_func_refs(source, node.closure_func_refs, all_nodes)
|
|
73
|
+
return source
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _SuperRewriter(ast.NodeTransformer):
|
|
77
|
+
"""Replace ``super()`` with ``super(ClassName, self)`` or ``super(ClassName, cls)``."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, class_name: str) -> None:
|
|
80
|
+
self._class_name = class_name
|
|
81
|
+
self._first_param: str | None = None
|
|
82
|
+
|
|
83
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
84
|
+
self._first_param = node.args.args[0].arg if node.args.args else "self"
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
self._first_param = None
|
|
87
|
+
return node
|
|
88
|
+
|
|
89
|
+
visit_AsyncFunctionDef = visit_FunctionDef # type: ignore[assignment]
|
|
90
|
+
|
|
91
|
+
def visit_Call(self, node: ast.Call) -> ast.Call:
|
|
92
|
+
self.generic_visit(node)
|
|
93
|
+
if (
|
|
94
|
+
isinstance(node.func, ast.Name)
|
|
95
|
+
and node.func.id == "super"
|
|
96
|
+
and not node.args
|
|
97
|
+
and not node.keywords
|
|
98
|
+
):
|
|
99
|
+
node.args = [
|
|
100
|
+
ast.Name(id=self._class_name, ctx=ast.Load()),
|
|
101
|
+
ast.Name(id=self._first_param or "self", ctx=ast.Load()),
|
|
102
|
+
]
|
|
103
|
+
return node
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _rewrite_bare_super(source: str, class_name: str) -> str:
|
|
107
|
+
"""Replace zero-arg ``super()`` with ``super(ClassName, self/cls)``."""
|
|
108
|
+
if "super()" not in source:
|
|
109
|
+
return source
|
|
110
|
+
tree = ast.parse(source)
|
|
111
|
+
tree = _SuperRewriter(class_name).visit(tree)
|
|
112
|
+
ast.fix_missing_locations(tree)
|
|
113
|
+
return ast.unparse(tree)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _indent_method(source: str) -> str:
|
|
117
|
+
"""Indent a method source for embedding inside a class block."""
|
|
118
|
+
return "\n".join(
|
|
119
|
+
(" " + line if line.strip() else "")
|
|
120
|
+
for line in source.rstrip().splitlines()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _build_class_block(
|
|
125
|
+
owner_class: str,
|
|
126
|
+
member_qnames: list[str],
|
|
127
|
+
nodes: dict[str, FunctionNode],
|
|
128
|
+
) -> str:
|
|
129
|
+
"""Build a complete ``class ... :`` block from member nodes."""
|
|
130
|
+
class_name = owner_class.rsplit(".", 1)[-1]
|
|
131
|
+
|
|
132
|
+
method_sources = [
|
|
133
|
+
_indent_method(_rewrite_bare_super(
|
|
134
|
+
_apply_closure_transforms(nodes[qname], nodes), class_name,
|
|
135
|
+
))
|
|
136
|
+
for qname in member_qnames
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
bases: list[str] = []
|
|
140
|
+
keywords: dict[str, str] = {}
|
|
141
|
+
class_attrs: list[str] = []
|
|
142
|
+
class_decorators: list[str] = []
|
|
143
|
+
for qname in member_qnames:
|
|
144
|
+
n = nodes[qname]
|
|
145
|
+
if n.class_bases and not bases:
|
|
146
|
+
bases = n.class_bases
|
|
147
|
+
if n.class_keywords and not keywords:
|
|
148
|
+
keywords = n.class_keywords
|
|
149
|
+
if n.class_attrs and not class_attrs:
|
|
150
|
+
class_attrs = n.class_attrs
|
|
151
|
+
if n.class_decorators and not class_decorators:
|
|
152
|
+
class_decorators = n.class_decorators
|
|
153
|
+
|
|
154
|
+
header_parts = list(bases)
|
|
155
|
+
for k, v in keywords.items():
|
|
156
|
+
header_parts.append(f"{k}={v}")
|
|
157
|
+
|
|
158
|
+
if header_parts:
|
|
159
|
+
header = f"class {class_name}({', '.join(header_parts)}):\n"
|
|
160
|
+
else:
|
|
161
|
+
header = f"class {class_name}:\n"
|
|
162
|
+
|
|
163
|
+
decorator_lines = "".join(f"@{d}\n" for d in class_decorators)
|
|
164
|
+
attr_block = "".join(
|
|
165
|
+
_indent_method(_rewrite_bare_super(attr, class_name)) + "\n\n"
|
|
166
|
+
for attr in class_attrs
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return decorator_lines + header + attr_block + "\n\n".join(method_sources)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class MergeResult:
|
|
174
|
+
"""Outcome of merging two stores."""
|
|
175
|
+
|
|
176
|
+
added_objects: int = 0
|
|
177
|
+
added_refs: int = 0
|
|
178
|
+
conflicts: dict[str, tuple[str, str]] = field(default_factory=dict)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Store:
|
|
182
|
+
"""Content-addressable store for function objects with separate topology.
|
|
183
|
+
|
|
184
|
+
Separates three concerns:
|
|
185
|
+
- **objects**: content-addressable blobs keyed by content hash
|
|
186
|
+
- **deps**: dependency adjacency list (hash -> [dep hashes])
|
|
187
|
+
- **refs**: named references (qualified name -> hash)
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self) -> None:
|
|
191
|
+
self._objects: dict[str, dict[str, Any]] = {}
|
|
192
|
+
self._deps: dict[str, list[str]] = {}
|
|
193
|
+
self._refs: dict[str, str] = {}
|
|
194
|
+
|
|
195
|
+
# -- Object operations ---------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def put(self, node: FunctionNode) -> str:
|
|
198
|
+
"""Store a node's content blob. Returns the content hash.
|
|
199
|
+
|
|
200
|
+
Does NOT set deps -- call :meth:`set_deps` separately.
|
|
201
|
+
"""
|
|
202
|
+
h = node.content_hash()
|
|
203
|
+
if h not in self._objects:
|
|
204
|
+
self._objects[h] = node.to_content_blob()
|
|
205
|
+
return h
|
|
206
|
+
|
|
207
|
+
def put_blob(self, h: str, blob: dict[str, Any]) -> None:
|
|
208
|
+
"""Store a pre-hashed content blob (used by deserialization)."""
|
|
209
|
+
self._objects[h] = blob
|
|
210
|
+
|
|
211
|
+
def get(self, h: str) -> dict[str, Any] | None:
|
|
212
|
+
"""Retrieve raw content blob by hash."""
|
|
213
|
+
return self._objects.get(h)
|
|
214
|
+
|
|
215
|
+
def has(self, h: str) -> bool:
|
|
216
|
+
"""Check whether a content hash exists in the store."""
|
|
217
|
+
return h in self._objects
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def object_hashes(self) -> set[str]:
|
|
221
|
+
"""Set of all content hashes currently in the store."""
|
|
222
|
+
return set(self._objects)
|
|
223
|
+
|
|
224
|
+
# -- Dep operations ------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def set_deps(self, h: str, dep_hashes: list[str]) -> None:
|
|
227
|
+
"""Set dependency edges for a content hash."""
|
|
228
|
+
if dep_hashes:
|
|
229
|
+
self._deps[h] = list(dep_hashes)
|
|
230
|
+
else:
|
|
231
|
+
self._deps.pop(h, None)
|
|
232
|
+
|
|
233
|
+
def get_deps(self, h: str) -> list[str]:
|
|
234
|
+
"""Get dependency hashes for a content hash."""
|
|
235
|
+
return list(self._deps.get(h, []))
|
|
236
|
+
|
|
237
|
+
# -- Ref operations ------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def set_ref(self, name: str, h: str) -> None:
|
|
240
|
+
"""Map a qualified name to a content hash."""
|
|
241
|
+
self._refs[name] = h
|
|
242
|
+
|
|
243
|
+
def get_ref(self, name: str) -> str | None:
|
|
244
|
+
"""Look up a content hash by qualified name."""
|
|
245
|
+
return self._refs.get(name)
|
|
246
|
+
|
|
247
|
+
def del_ref(self, name: str) -> None:
|
|
248
|
+
"""Remove a named reference."""
|
|
249
|
+
self._refs.pop(name, None)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def refs(self) -> dict[str, str]:
|
|
253
|
+
"""Snapshot of all named references (qualified name -> hash)."""
|
|
254
|
+
return dict(self._refs)
|
|
255
|
+
|
|
256
|
+
# -- Graph operations ----------------------------------------------------
|
|
257
|
+
|
|
258
|
+
def walk(self, root_hash: str) -> list[str]:
|
|
259
|
+
"""BFS walk returning all reachable hashes from *root_hash*."""
|
|
260
|
+
visited: dict[str, None] = {}
|
|
261
|
+
stack = [root_hash]
|
|
262
|
+
while stack:
|
|
263
|
+
h = stack.pop()
|
|
264
|
+
if h in visited:
|
|
265
|
+
continue
|
|
266
|
+
visited[h] = None
|
|
267
|
+
stack.extend(self._deps.get(h, []))
|
|
268
|
+
return list(visited)
|
|
269
|
+
|
|
270
|
+
def subgraph(self, *root_hashes: str) -> "Store":
|
|
271
|
+
"""Extract transitive closure of *root_hashes* into a new store."""
|
|
272
|
+
reachable: set[str] = set()
|
|
273
|
+
for root in root_hashes:
|
|
274
|
+
reachable.update(self.walk(root))
|
|
275
|
+
|
|
276
|
+
store = Store()
|
|
277
|
+
hash_to_qname = {h: qn for qn, h in self._refs.items()}
|
|
278
|
+
for h in reachable:
|
|
279
|
+
blob = self._objects.get(h)
|
|
280
|
+
if blob is not None:
|
|
281
|
+
store.put_blob(h, dict(blob))
|
|
282
|
+
deps = self._deps.get(h)
|
|
283
|
+
if deps:
|
|
284
|
+
store.set_deps(h, [d for d in deps if d in reachable])
|
|
285
|
+
qn = hash_to_qname.get(h)
|
|
286
|
+
if qn is not None:
|
|
287
|
+
store.set_ref(qn, h)
|
|
288
|
+
return store
|
|
289
|
+
|
|
290
|
+
def missing(self, hashes: set[str]) -> set[str]:
|
|
291
|
+
"""Return hashes from *hashes* not present in this store."""
|
|
292
|
+
return hashes - self._objects.keys()
|
|
293
|
+
|
|
294
|
+
def merge(self, other: "Store") -> MergeResult:
|
|
295
|
+
"""Merge *other* into this store.
|
|
296
|
+
|
|
297
|
+
Objects are unioned by hash (same hash = same content).
|
|
298
|
+
Deps are unioned per hash.
|
|
299
|
+
Refs: existing refs are kept on conflict; conflicts are reported.
|
|
300
|
+
"""
|
|
301
|
+
result = MergeResult()
|
|
302
|
+
|
|
303
|
+
for h, blob in other._objects.items():
|
|
304
|
+
if h not in self._objects:
|
|
305
|
+
self._objects[h] = dict(blob)
|
|
306
|
+
result.added_objects += 1
|
|
307
|
+
|
|
308
|
+
for h, deps in other._deps.items():
|
|
309
|
+
existing = self._deps.get(h)
|
|
310
|
+
if existing is None:
|
|
311
|
+
self._deps[h] = list(deps)
|
|
312
|
+
else:
|
|
313
|
+
merged = list(dict.fromkeys(existing + deps))
|
|
314
|
+
self._deps[h] = merged
|
|
315
|
+
|
|
316
|
+
for name, h in other._refs.items():
|
|
317
|
+
existing_ref = self._refs.get(name)
|
|
318
|
+
if existing_ref is None:
|
|
319
|
+
self._refs[name] = h
|
|
320
|
+
result.added_refs += 1
|
|
321
|
+
elif existing_ref != h:
|
|
322
|
+
result.conflicts[name] = (existing_ref, h)
|
|
323
|
+
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
def gc(self) -> set[str]:
|
|
327
|
+
"""Remove objects unreachable from any ref. Returns removed hashes."""
|
|
328
|
+
reachable: set[str] = set()
|
|
329
|
+
for h in self._refs.values():
|
|
330
|
+
reachable.update(self.walk(h))
|
|
331
|
+
garbage = self._objects.keys() - reachable
|
|
332
|
+
for h in garbage:
|
|
333
|
+
del self._objects[h]
|
|
334
|
+
self._deps.pop(h, None)
|
|
335
|
+
return garbage
|
|
336
|
+
|
|
337
|
+
# -- Reconstruction ------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
def collect(self, function_name: str) -> tuple[str, dict[str, FunctionNode]]:
|
|
340
|
+
"""Resolve *function_name*, walk deps, return (target_qname, nodes).
|
|
341
|
+
|
|
342
|
+
Raises :class:`KeyError` if *function_name* is not found.
|
|
343
|
+
"""
|
|
344
|
+
hash_to_qname = {h: qn for qn, h in self._refs.items()}
|
|
345
|
+
target_hash = self._resolve_function_hash(function_name)
|
|
346
|
+
reachable = self.walk(target_hash)
|
|
347
|
+
|
|
348
|
+
needed: dict[str, FunctionNode] = {}
|
|
349
|
+
for content_hash in reachable:
|
|
350
|
+
blob = self._objects.get(content_hash)
|
|
351
|
+
if blob is None:
|
|
352
|
+
continue
|
|
353
|
+
needed[hash_to_qname.get(content_hash, f"{blob['module']}.{blob['name']}")] = (
|
|
354
|
+
self._blob_to_node(content_hash, blob, hash_to_qname)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
target_qname = hash_to_qname.get(target_hash, f"unknown.{function_name}")
|
|
358
|
+
return target_qname, needed
|
|
359
|
+
|
|
360
|
+
def _blob_to_node(
|
|
361
|
+
self,
|
|
362
|
+
content_hash: str,
|
|
363
|
+
blob: dict[str, Any],
|
|
364
|
+
hash_to_qname: dict[str, str],
|
|
365
|
+
) -> FunctionNode:
|
|
366
|
+
"""Convert a raw content blob into a FunctionNode."""
|
|
367
|
+
qname = hash_to_qname.get(content_hash, f"{blob['module']}.{blob['name']}")
|
|
368
|
+
closure_func_refs = {
|
|
369
|
+
var: hash_to_qname.get(ref_hash, ref_hash)
|
|
370
|
+
for var, ref_hash in blob.get("closure_func_refs", {}).items()
|
|
371
|
+
}
|
|
372
|
+
dep_qnames = [
|
|
373
|
+
hash_to_qname.get(dep_hash, dep_hash)
|
|
374
|
+
for dep_hash in self._deps.get(content_hash, [])
|
|
375
|
+
]
|
|
376
|
+
return FunctionNode(
|
|
377
|
+
qualified_name=qname,
|
|
378
|
+
name=blob["name"],
|
|
379
|
+
module=blob["module"],
|
|
380
|
+
source=blob["source"],
|
|
381
|
+
imports=[ImportInfo.from_dict(imp) for imp in blob["imports"]],
|
|
382
|
+
dependencies=dep_qnames,
|
|
383
|
+
owner_class=blob.get("owner_class"),
|
|
384
|
+
closure_vars=blob.get("closure_vars", {}),
|
|
385
|
+
closure_func_refs=closure_func_refs,
|
|
386
|
+
module_vars=blob.get("module_vars", {}),
|
|
387
|
+
class_bases=blob.get("class_bases", []),
|
|
388
|
+
class_keywords=blob.get("class_keywords", {}),
|
|
389
|
+
class_attrs=blob.get("class_attrs", []),
|
|
390
|
+
class_decorators=blob.get("class_decorators", []),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def reconstruct(self, function_name: str) -> str:
|
|
394
|
+
"""Reconstruct executable Python source for *function_name*."""
|
|
395
|
+
target_qname, needed = self.collect(function_name)
|
|
396
|
+
|
|
397
|
+
logger.debug(
|
|
398
|
+
"Reconstructing %s: %d dependencies",
|
|
399
|
+
target_qname, len(needed) - 1,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
order = _topological_order(needed)
|
|
403
|
+
logger.debug("Topological order: %s", order)
|
|
404
|
+
|
|
405
|
+
import_lines = _collect_imports(needed, order)
|
|
406
|
+
module_var_lines = _collect_module_vars(needed, order)
|
|
407
|
+
class_groups = _group_by_class(needed, order)
|
|
408
|
+
|
|
409
|
+
parts: list[str] = []
|
|
410
|
+
if import_lines:
|
|
411
|
+
parts.append("\n".join(import_lines))
|
|
412
|
+
if module_var_lines:
|
|
413
|
+
parts.append("\n".join(module_var_lines))
|
|
414
|
+
|
|
415
|
+
emitted_classes: set[str] = set()
|
|
416
|
+
for qname in order:
|
|
417
|
+
node = needed[qname]
|
|
418
|
+
source = _apply_closure_transforms(node, needed)
|
|
419
|
+
if node.owner_class is None:
|
|
420
|
+
parts.append(source.rstrip())
|
|
421
|
+
elif node.owner_class not in emitted_classes:
|
|
422
|
+
emitted_classes.add(node.owner_class)
|
|
423
|
+
parts.append(_build_class_block(
|
|
424
|
+
node.owner_class, class_groups[node.owner_class], needed,
|
|
425
|
+
))
|
|
426
|
+
|
|
427
|
+
return "\n\n\n".join(parts) + "\n"
|
|
428
|
+
|
|
429
|
+
# -- Serialization -------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
def to_dict(self) -> dict[str, Any]:
|
|
432
|
+
"""Export as a dict in v0.4.0 format."""
|
|
433
|
+
qname_to_hash = self._refs
|
|
434
|
+
|
|
435
|
+
# Build objects with closure_func_refs converted to hashes
|
|
436
|
+
objects: dict[str, dict[str, Any]] = {}
|
|
437
|
+
for h, blob in self._objects.items():
|
|
438
|
+
out = dict(blob)
|
|
439
|
+
if "closure_func_refs" in out:
|
|
440
|
+
out["closure_func_refs"] = {
|
|
441
|
+
var: qname_to_hash.get(ref_qn, ref_qn)
|
|
442
|
+
for var, ref_qn in out["closure_func_refs"].items()
|
|
443
|
+
}
|
|
444
|
+
objects[h] = out
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
"version": _VERSION,
|
|
448
|
+
"objects": objects,
|
|
449
|
+
"deps": {h: list(d) for h, d in self._deps.items()},
|
|
450
|
+
"refs": dict(self._refs),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
def to_json(self) -> str:
|
|
454
|
+
"""Serialize the store to a JSON string."""
|
|
455
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
459
|
+
"""Deserialize a store from a dict (as produced by :meth:`to_dict`)."""
|
|
460
|
+
store = cls()
|
|
461
|
+
refs = data.get("refs", {})
|
|
462
|
+
hash_to_qname = {h: qn for qn, h in refs.items()}
|
|
463
|
+
|
|
464
|
+
for h, blob in data.get("objects", {}).items():
|
|
465
|
+
out = dict(blob)
|
|
466
|
+
# Convert closure_func_refs hashes back to qnames
|
|
467
|
+
if "closure_func_refs" in out:
|
|
468
|
+
out["closure_func_refs"] = {
|
|
469
|
+
var: hash_to_qname.get(ref_h, ref_h)
|
|
470
|
+
for var, ref_h in out["closure_func_refs"].items()
|
|
471
|
+
}
|
|
472
|
+
store.put_blob(h, out)
|
|
473
|
+
|
|
474
|
+
for h, dep_list in data.get("deps", {}).items():
|
|
475
|
+
store.set_deps(h, dep_list)
|
|
476
|
+
|
|
477
|
+
for name, h in refs.items():
|
|
478
|
+
store.set_ref(name, h)
|
|
479
|
+
|
|
480
|
+
return store
|
|
481
|
+
|
|
482
|
+
@classmethod
|
|
483
|
+
def from_json(cls, json_str: str) -> Self:
|
|
484
|
+
"""Deserialize a store from a JSON string."""
|
|
485
|
+
return cls.from_dict(json.loads(json_str))
|
|
486
|
+
|
|
487
|
+
# -- Private helpers -----------------------------------------------------
|
|
488
|
+
|
|
489
|
+
def _resolve_function_hash(self, function_name: str) -> str:
|
|
490
|
+
"""Resolve a function name (qualified or simple) to its hash."""
|
|
491
|
+
if function_name in self._refs:
|
|
492
|
+
return self._refs[function_name]
|
|
493
|
+
for qn, h in self._refs.items():
|
|
494
|
+
blob = self._objects.get(h)
|
|
495
|
+
if blob and blob["name"] == function_name:
|
|
496
|
+
return h
|
|
497
|
+
raise KeyError(
|
|
498
|
+
f"Function '{function_name}' not found in store"
|
|
499
|
+
)
|
|
500
|
+
|