py2dag 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: py2dag
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Convert Python function plans to DAG (JSON, pseudo, optional SVG).
5
5
  License: MIT
6
6
  Author: rvergis
@@ -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:
@@ -310,7 +353,8 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
310
353
  raise DSLParseError("output requires as=\"filename\"")
311
354
  outputs.append({"from": ssa_from, "as": filename})
312
355
  else:
313
- raise DSLParseError("Only settings() and output() calls allowed as expressions")
356
+ # General expression call: represent as an op node too
357
+ _emit_expr_call(call)
314
358
  return None
315
359
  elif isinstance(stmt, ast.Return):
316
360
  if isinstance(stmt.value, ast.Name):
@@ -411,6 +455,29 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
411
455
  ctx_counts["loop"] += 1
412
456
  context_suffix = f"loop{ctx_counts['loop']}"
413
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)
414
481
  for inner in stmt.body:
415
482
  _parse_stmt(inner)
416
483
  versions_body, latest_body = versions, latest
@@ -419,6 +486,16 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
419
486
  # Add iter dep to first op in body
420
487
  if len(ops) > body_ops_start:
421
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
+ })
422
499
  # Loop-carried vars: only those existing pre-loop and reassigned in body
423
500
  changed = {k for k in latest_body if pre_latest.get(k) != latest_body.get(k)}
424
501
  carried = [k for k in changed if k in pre_latest]
@@ -461,7 +538,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
461
538
  "args": {"var": var},
462
539
  })
463
540
  return None
464
- elif isinstance(stmt, (ast.Pass,)):
541
+ elif isinstance(stmt, (ast.Pass, ast.Continue, ast.Break)):
465
542
  return None
466
543
  else:
467
544
  raise DSLParseError("Only assignments, control flow, settings/output calls, and return are allowed in function body")
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "py2dag"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Convert Python function plans to DAG (JSON, pseudo, optional SVG)."
9
9
  authors = ["rvergis"]
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes