py2dag 0.2.2__py3-none-any.whl → 0.2.4__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.
py2dag/parser.py CHANGED
@@ -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:
@@ -245,6 +288,26 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
245
288
  })
246
289
  return ssa
247
290
 
291
+ def _emit_assign_from_subscript(var_name: str, node: ast.Subscript) -> str:
292
+ # Support name[key] where key is a JSON-serialisable literal
293
+ base = node.value
294
+ if not isinstance(base, ast.Name):
295
+ raise DSLParseError("Subscript base must be a variable name")
296
+ # Extract slice expression across Python versions
297
+ sl = getattr(node, 'slice', None)
298
+ # In Python >=3.9, slice is the actual node; before it may be ast.Index
299
+ if hasattr(ast, 'Index') and isinstance(sl, getattr(ast, 'Index')): # type: ignore[attr-defined]
300
+ sl = sl.value # type: ignore[assignment]
301
+ key = _literal(sl) # may raise if not literal
302
+ ssa = _ssa_new(var_name)
303
+ ops.append({
304
+ "id": ssa,
305
+ "op": "GET.item",
306
+ "deps": [_ssa_get(base.id)],
307
+ "args": {"key": key},
308
+ })
309
+ return ssa
310
+
248
311
  def _emit_cond(node: ast.AST, kind: str = "if") -> str:
249
312
  expr = _stringify(node)
250
313
  deps = [_ssa_get(n) for n in _collect_value_deps(node)]
@@ -279,6 +342,8 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
279
342
  return _emit_assign_from_literal_or_pack(var_name, value)
280
343
  elif isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
281
344
  return _emit_assign_from_comp(var_name, value)
345
+ elif isinstance(value, ast.Subscript):
346
+ return _emit_assign_from_subscript(var_name, value)
282
347
  else:
283
348
  raise DSLParseError("Right hand side must be a call or f-string")
284
349
  elif isinstance(stmt, ast.Expr):
@@ -310,7 +375,8 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
310
375
  raise DSLParseError("output requires as=\"filename\"")
311
376
  outputs.append({"from": ssa_from, "as": filename})
312
377
  else:
313
- raise DSLParseError("Only settings() and output() calls allowed as expressions")
378
+ # General expression call: represent as an op node too
379
+ _emit_expr_call(call)
314
380
  return None
315
381
  elif isinstance(stmt, ast.Return):
316
382
  if isinstance(stmt.value, ast.Name):
@@ -411,6 +477,29 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
411
477
  ctx_counts["loop"] += 1
412
478
  context_suffix = f"loop{ctx_counts['loop']}"
413
479
  versions, latest = versions_body, latest_body
480
+ # Predefine loop target variables as items from iterator for dependency resolution
481
+ def _bind_loop_target(target: ast.AST):
482
+ if isinstance(target, ast.Name):
483
+ ssa_item = _ssa_new(target.id)
484
+ ops.append({
485
+ "id": ssa_item,
486
+ "op": "ITER.item",
487
+ "deps": [iter_id],
488
+ "args": {"target": target.id},
489
+ })
490
+ elif isinstance(target, ast.Tuple):
491
+ for elt in target.elts:
492
+ if isinstance(elt, ast.Name):
493
+ ssa_item = _ssa_new(elt.id)
494
+ ops.append({
495
+ "id": ssa_item,
496
+ "op": "ITER.item",
497
+ "deps": [iter_id],
498
+ "args": {"target": elt.id},
499
+ })
500
+ # Other patterns are ignored for now
501
+
502
+ _bind_loop_target(stmt.target)
414
503
  for inner in stmt.body:
415
504
  _parse_stmt(inner)
416
505
  versions_body, latest_body = versions, latest
@@ -419,6 +508,16 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
419
508
  # Add iter dep to first op in body
420
509
  if len(ops) > body_ops_start:
421
510
  ops[body_ops_start]["deps"] = [*ops[body_ops_start].get("deps", []), iter_id]
511
+ # Emit a summary foreach comp node depending on iterable value deps
512
+ iter_name_deps = _collect_value_deps(stmt.iter)
513
+ foreach_deps = [_ssa_get(n) for n in iter_name_deps]
514
+ ssa_foreach = _ssa_new("foreach")
515
+ ops.append({
516
+ "id": ssa_foreach,
517
+ "op": "COMP.foreach",
518
+ "deps": foreach_deps,
519
+ "args": {"target": t_label or ""},
520
+ })
422
521
  # Loop-carried vars: only those existing pre-loop and reassigned in body
423
522
  changed = {k for k in latest_body if pre_latest.get(k) != latest_body.get(k)}
424
523
  carried = [k for k in changed if k in pre_latest]
@@ -461,7 +560,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
461
560
  "args": {"var": var},
462
561
  })
463
562
  return None
464
- elif isinstance(stmt, (ast.Pass,)):
563
+ elif isinstance(stmt, (ast.Pass, ast.Continue, ast.Break)):
465
564
  return None
466
565
  else:
467
566
  raise DSLParseError("Only assignments, control flow, settings/output calls, and return are allowed in function body")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: py2dag
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Convert Python function plans to DAG (JSON, pseudo, optional SVG).
5
5
  License: MIT
6
6
  Author: rvergis
@@ -2,10 +2,10 @@ py2dag/__init__.py,sha256=i8VB44JCVcRJAcvnQtbH8YVRUz5j7dE355iRbikXPGQ,250
2
2
  py2dag/cli.py,sha256=BCBi5mNxOqeEN8uEMt_hiDx0iSt7ZE3y74cGXREzZ2I,1296
3
3
  py2dag/export_dagre.py,sha256=lScDQCIh5nE3RI_ac4Ye8AsLnn3MIn-fm1WlqP9Ei4U,4608
4
4
  py2dag/export_svg.py,sha256=tKqh16hCrCE1QJ9-MTue1ELRxmXOb2akokpSz_a5eE8,2383
5
- py2dag/parser.py,sha256=XTs1ItzAmXkAvpJX1BfyUlI25kMJp-GVqbYnpU9wdP4,23675
5
+ py2dag/parser.py,sha256=mJX_rjjSnO7G0-3thh5bBpPlG8N0IWAUqnp4Yn7Vd3A,28711
6
6
  py2dag/pseudo.py,sha256=NJK61slyFLtSjhj8gJDJneUInEpBN57_41g8IfHNPWI,922
7
- py2dag-0.2.2.dist-info/LICENSE,sha256=3Qee1EPwej_nusovTbyIQ8LvD2rXHdM0c6LNwk_D8Kc,1067
8
- py2dag-0.2.2.dist-info/METADATA,sha256=0HlPv8M1BBtNm0o-W1qJv7kI0S7J-UYOnTCrCGXnjSc,3549
9
- py2dag-0.2.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
- py2dag-0.2.2.dist-info/entry_points.txt,sha256=Q0SHexJJ0z1te4AYL1xTZogx5FrxCCE1ZJ5qntkFMZs,42
11
- py2dag-0.2.2.dist-info/RECORD,,
7
+ py2dag-0.2.4.dist-info/LICENSE,sha256=3Qee1EPwej_nusovTbyIQ8LvD2rXHdM0c6LNwk_D8Kc,1067
8
+ py2dag-0.2.4.dist-info/METADATA,sha256=yNQ2eR_WmySOQGRyq0uzvuYeKzV5_AxH2FTlSfPIvd0,3549
9
+ py2dag-0.2.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
+ py2dag-0.2.4.dist-info/entry_points.txt,sha256=Q0SHexJJ0z1te4AYL1xTZogx5FrxCCE1ZJ5qntkFMZs,42
11
+ py2dag-0.2.4.dist-info/RECORD,,
File without changes