py2dag 0.2.1__tar.gz → 0.2.3__tar.gz
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.
- {py2dag-0.2.1 → py2dag-0.2.3}/PKG-INFO +1 -1
- {py2dag-0.2.1 → py2dag-0.2.3}/py2dag/export_dagre.py +13 -2
- {py2dag-0.2.1 → py2dag-0.2.3}/py2dag/export_svg.py +19 -2
- {py2dag-0.2.1 → py2dag-0.2.3}/py2dag/parser.py +97 -5
- {py2dag-0.2.1 → py2dag-0.2.3}/pyproject.toml +1 -1
- {py2dag-0.2.1 → py2dag-0.2.3}/LICENSE +0 -0
- {py2dag-0.2.1 → py2dag-0.2.3}/README.md +0 -0
- {py2dag-0.2.1 → py2dag-0.2.3}/py2dag/__init__.py +0 -0
- {py2dag-0.2.1 → py2dag-0.2.3}/py2dag/cli.py +0 -0
- {py2dag-0.2.1 → py2dag-0.2.3}/py2dag/pseudo.py +0 -0
@@ -73,10 +73,21 @@ HTML_TEMPLATE = """<!doctype html>
|
|
73
73
|
g.setEdge(out.from, outId);
|
74
74
|
});
|
75
75
|
|
76
|
-
//
|
76
|
+
// Build index for source op lookup
|
77
|
+
const opById = {};
|
78
|
+
(plan.ops || []).forEach(op => { opById[op.id] = op; });
|
79
|
+
|
80
|
+
// Add dependency edges between ops with labels
|
77
81
|
(plan.ops || []).forEach(op => {
|
78
82
|
(op.deps || []).forEach(dep => {
|
79
|
-
|
83
|
+
const src = opById[dep];
|
84
|
+
let edgeLabel = dep; // default to SSA id
|
85
|
+
if (src && src.op === 'COND.eval') {
|
86
|
+
edgeLabel = 'cond';
|
87
|
+
} else if (src && src.op === 'ITER.eval') {
|
88
|
+
edgeLabel = (src.args && src.args.target) ? src.args.target : 'iter';
|
89
|
+
}
|
90
|
+
g.setEdge(dep, op.id, { label: edgeLabel });
|
80
91
|
});
|
81
92
|
});
|
82
93
|
|
@@ -22,10 +22,27 @@ def export(plan: Dict[str, Any], filename: str = "plan.svg") -> str:
|
|
22
22
|
raise RuntimeError("Python package 'graphviz' is required for SVG export")
|
23
23
|
|
24
24
|
graph = Digraph(format="svg")
|
25
|
-
|
25
|
+
|
26
|
+
# Index ops for edge label decisions
|
27
|
+
ops = list(plan.get("ops", []))
|
28
|
+
op_by_id = {op["id"]: op for op in ops}
|
29
|
+
|
30
|
+
# Nodes
|
31
|
+
for op in ops:
|
26
32
|
graph.node(op["id"], label=op["op"])
|
33
|
+
|
34
|
+
# Dependency edges with labels showing data/control
|
35
|
+
for op in ops:
|
27
36
|
for dep in op.get("deps", []):
|
28
|
-
|
37
|
+
src = op_by_id.get(dep)
|
38
|
+
label = dep # default to SSA id
|
39
|
+
if src is not None:
|
40
|
+
if src.get("op") == "COND.eval":
|
41
|
+
label = "cond"
|
42
|
+
elif src.get("op") == "ITER.eval":
|
43
|
+
args = src.get("args", {}) or {}
|
44
|
+
label = str(args.get("target") or "iter")
|
45
|
+
graph.edge(dep, op["id"], label=label)
|
29
46
|
for out in plan.get("outputs", []):
|
30
47
|
out_id = f"out:{out['as']}"
|
31
48
|
graph.node(out_id, label=out['as'], shape="note")
|
@@ -173,6 +173,10 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
173
173
|
ops.append({"id": ssa, "op": op_name, "deps": deps, "args": kwargs})
|
174
174
|
return ssa
|
175
175
|
|
176
|
+
def _emit_expr_call(call: ast.Call) -> str:
|
177
|
+
"""Emit a node for a bare expression call (no assignment)."""
|
178
|
+
return _emit_assign_from_call("call", call)
|
179
|
+
|
176
180
|
def _emit_assign_from_fstring(var_name: str, fstr: ast.JoinedStr) -> str:
|
177
181
|
deps: List[str] = []
|
178
182
|
parts: List[str] = []
|
@@ -222,6 +226,45 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
222
226
|
"args": {},
|
223
227
|
})
|
224
228
|
return ssa
|
229
|
+
if isinstance(value, ast.Dict):
|
230
|
+
# Support dict with values from names/calls or literals by synthesizing nodes
|
231
|
+
keys: List[str] = []
|
232
|
+
deps: List[str] = []
|
233
|
+
for k_node, v_node in zip(value.keys, value.values):
|
234
|
+
k_str = _literal(k_node)
|
235
|
+
if not isinstance(k_str, (str, int, float, bool)):
|
236
|
+
k_str = str(k_str)
|
237
|
+
keys.append(str(k_str))
|
238
|
+
if isinstance(v_node, ast.Name):
|
239
|
+
deps.append(_ssa_get(v_node.id))
|
240
|
+
elif isinstance(v_node, ast.Await):
|
241
|
+
inner = v_node.value
|
242
|
+
if not isinstance(inner, ast.Call):
|
243
|
+
raise DSLParseError("await must wrap a call in dict value")
|
244
|
+
tmp_id = _emit_assign_from_call(f"{var_name}_field", inner)
|
245
|
+
deps.append(tmp_id)
|
246
|
+
elif isinstance(v_node, ast.Call):
|
247
|
+
tmp_id = _emit_assign_from_call(f"{var_name}_field", v_node)
|
248
|
+
deps.append(tmp_id)
|
249
|
+
else:
|
250
|
+
# Synthesize const for literal value
|
251
|
+
lit_val = _literal(v_node)
|
252
|
+
tmp = _ssa_new(f"{var_name}_lit")
|
253
|
+
ops.append({
|
254
|
+
"id": tmp,
|
255
|
+
"op": "CONST.value",
|
256
|
+
"deps": [],
|
257
|
+
"args": {"value": lit_val},
|
258
|
+
})
|
259
|
+
deps.append(tmp)
|
260
|
+
ssa = _ssa_new(var_name)
|
261
|
+
ops.append({
|
262
|
+
"id": ssa,
|
263
|
+
"op": "PACK.dict",
|
264
|
+
"deps": deps,
|
265
|
+
"args": {"keys": keys},
|
266
|
+
})
|
267
|
+
return ssa
|
225
268
|
raise
|
226
269
|
|
227
270
|
def _emit_assign_from_comp(var_name: str, node: ast.AST) -> str:
|
@@ -252,11 +295,14 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
252
295
|
ops.append({"id": ssa, "op": "COND.eval", "deps": deps, "args": {"expr": expr, "kind": kind}})
|
253
296
|
return ssa
|
254
297
|
|
255
|
-
def _emit_iter(node: ast.AST) -> str:
|
298
|
+
def _emit_iter(node: ast.AST, target_label: Optional[str] = None) -> str:
|
256
299
|
expr = _stringify(node)
|
257
300
|
deps = [_ssa_get(n) for n in _collect_value_deps(node)]
|
258
301
|
ssa = _ssa_new("iter")
|
259
|
-
|
302
|
+
args = {"expr": expr, "kind": "for"}
|
303
|
+
if target_label:
|
304
|
+
args["target"] = target_label
|
305
|
+
ops.append({"id": ssa, "op": "ITER.eval", "deps": deps, "args": args})
|
260
306
|
return ssa
|
261
307
|
|
262
308
|
def _parse_stmt(stmt: ast.stmt) -> Optional[str]:
|
@@ -307,7 +353,8 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
307
353
|
raise DSLParseError("output requires as=\"filename\"")
|
308
354
|
outputs.append({"from": ssa_from, "as": filename})
|
309
355
|
else:
|
310
|
-
|
356
|
+
# General expression call: represent as an op node too
|
357
|
+
_emit_expr_call(call)
|
311
358
|
return None
|
312
359
|
elif isinstance(stmt, ast.Return):
|
313
360
|
if isinstance(stmt.value, ast.Name):
|
@@ -383,7 +430,19 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
383
430
|
return None
|
384
431
|
elif isinstance(stmt, (ast.For, ast.AsyncFor)):
|
385
432
|
# ITER over iterable
|
386
|
-
|
433
|
+
# Determine loop target label if simple
|
434
|
+
t = stmt.target
|
435
|
+
t_label: Optional[str] = None
|
436
|
+
if isinstance(t, ast.Name):
|
437
|
+
t_label = t.id
|
438
|
+
elif isinstance(t, ast.Tuple) and all(isinstance(e, ast.Name) for e in t.elts):
|
439
|
+
t_label = ",".join(e.id for e in t.elts) # type: ignore[attr-defined]
|
440
|
+
else:
|
441
|
+
try:
|
442
|
+
t_label = ast.unparse(t) # type: ignore[attr-defined]
|
443
|
+
except Exception:
|
444
|
+
t_label = None
|
445
|
+
iter_id = _emit_iter(stmt.iter, target_label=t_label)
|
387
446
|
# Save pre-loop state
|
388
447
|
pre_versions = dict(versions)
|
389
448
|
pre_latest = dict(latest)
|
@@ -396,6 +455,29 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
396
455
|
ctx_counts["loop"] += 1
|
397
456
|
context_suffix = f"loop{ctx_counts['loop']}"
|
398
457
|
versions, latest = versions_body, latest_body
|
458
|
+
# Predefine loop target variables as items from iterator for dependency resolution
|
459
|
+
def _bind_loop_target(target: ast.AST):
|
460
|
+
if isinstance(target, ast.Name):
|
461
|
+
ssa_item = _ssa_new(target.id)
|
462
|
+
ops.append({
|
463
|
+
"id": ssa_item,
|
464
|
+
"op": "ITER.item",
|
465
|
+
"deps": [iter_id],
|
466
|
+
"args": {"target": target.id},
|
467
|
+
})
|
468
|
+
elif isinstance(target, ast.Tuple):
|
469
|
+
for elt in target.elts:
|
470
|
+
if isinstance(elt, ast.Name):
|
471
|
+
ssa_item = _ssa_new(elt.id)
|
472
|
+
ops.append({
|
473
|
+
"id": ssa_item,
|
474
|
+
"op": "ITER.item",
|
475
|
+
"deps": [iter_id],
|
476
|
+
"args": {"target": elt.id},
|
477
|
+
})
|
478
|
+
# Other patterns are ignored for now
|
479
|
+
|
480
|
+
_bind_loop_target(stmt.target)
|
399
481
|
for inner in stmt.body:
|
400
482
|
_parse_stmt(inner)
|
401
483
|
versions_body, latest_body = versions, latest
|
@@ -404,6 +486,16 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
404
486
|
# Add iter dep to first op in body
|
405
487
|
if len(ops) > body_ops_start:
|
406
488
|
ops[body_ops_start]["deps"] = [*ops[body_ops_start].get("deps", []), iter_id]
|
489
|
+
# Emit a summary foreach comp node depending on iterable value deps
|
490
|
+
iter_name_deps = _collect_value_deps(stmt.iter)
|
491
|
+
foreach_deps = [_ssa_get(n) for n in iter_name_deps]
|
492
|
+
ssa_foreach = _ssa_new("foreach")
|
493
|
+
ops.append({
|
494
|
+
"id": ssa_foreach,
|
495
|
+
"op": "COMP.foreach",
|
496
|
+
"deps": foreach_deps,
|
497
|
+
"args": {"target": t_label or ""},
|
498
|
+
})
|
407
499
|
# Loop-carried vars: only those existing pre-loop and reassigned in body
|
408
500
|
changed = {k for k in latest_body if pre_latest.get(k) != latest_body.get(k)}
|
409
501
|
carried = [k for k in changed if k in pre_latest]
|
@@ -446,7 +538,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
446
538
|
"args": {"var": var},
|
447
539
|
})
|
448
540
|
return None
|
449
|
-
elif isinstance(stmt, (ast.Pass,)):
|
541
|
+
elif isinstance(stmt, (ast.Pass, ast.Continue, ast.Break)):
|
450
542
|
return None
|
451
543
|
else:
|
452
544
|
raise DSLParseError("Only assignments, control flow, settings/output calls, and return are allowed in function body")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|