py2dag 0.3.3__tar.gz → 0.3.4__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.3.3
3
+ Version: 0.3.4
4
4
  Summary: Convert Python function plans to DAG (JSON, pseudo, optional SVG).
5
5
  License: MIT
6
6
  Author: rvergis
@@ -63,6 +63,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
63
63
  latest: Dict[str, str] = {}
64
64
  context_suffix: str = ""
65
65
  ctx_counts: Dict[str, int] = {"if": 0, "loop": 0, "while": 0, "except": 0}
66
+ loop_depth = 0
66
67
 
67
68
  def _ssa_new(name: str) -> str:
68
69
  if not VALID_NAME_RE.match(name):
@@ -331,6 +332,48 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
331
332
  })
332
333
  return ssa
333
334
 
335
+ def _emit_assign_to_subscript(target: ast.Subscript, value: ast.AST) -> str:
336
+ """Emit a SET.item node for an assignment like ``name[key] = value``."""
337
+ base = target.value
338
+ if not isinstance(base, ast.Name):
339
+ raise DSLParseError("Subscript base must be a variable name")
340
+ # Extract slice expression across Python versions
341
+ sl = getattr(target, "slice", None)
342
+ if hasattr(ast, "Index") and isinstance(sl, getattr(ast, "Index")): # type: ignore[attr-defined]
343
+ sl = sl.value # type: ignore[assignment]
344
+ key = _literal(sl)
345
+
346
+ awaited = False
347
+ if isinstance(value, ast.Await):
348
+ value = value.value
349
+ awaited = True
350
+
351
+ # Determine SSA id for RHS value
352
+ if isinstance(value, ast.Call):
353
+ val_id = _emit_assign_from_call(f"{base.id}_item", value, awaited)
354
+ elif isinstance(value, ast.JoinedStr):
355
+ val_id = _emit_assign_from_fstring(f"{base.id}_item", value)
356
+ elif isinstance(value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
357
+ val_id = _emit_assign_from_literal_or_pack(f"{base.id}_item", value)
358
+ elif isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
359
+ val_id = _emit_assign_from_comp(f"{base.id}_item", value)
360
+ elif isinstance(value, ast.Subscript):
361
+ val_id = _emit_assign_from_subscript(f"{base.id}_item", value)
362
+ elif isinstance(value, ast.Name):
363
+ val_id = _ssa_get(value.id)
364
+ else:
365
+ raise DSLParseError("Right hand side must be a call or f-string")
366
+
367
+ base_id = _ssa_get(base.id)
368
+ ssa = _ssa_new(base.id)
369
+ ops.append({
370
+ "id": ssa,
371
+ "op": "SET.item",
372
+ "deps": [base_id, val_id],
373
+ "args": {"key": key},
374
+ })
375
+ return ssa
376
+
334
377
  def _emit_cond(node: ast.AST, kind: str = "if") -> str:
335
378
  expr = _stringify(node)
336
379
  deps = [_ssa_get(n) for n in _collect_value_deps(node)]
@@ -349,28 +392,34 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
349
392
  return ssa
350
393
 
351
394
  def _parse_stmt(stmt: ast.stmt) -> Optional[str]:
352
- nonlocal returned_var, versions, latest, context_suffix
395
+ nonlocal returned_var, versions, latest, context_suffix, loop_depth
353
396
  if isinstance(stmt, ast.Assign):
354
- if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name):
355
- raise DSLParseError("Assignment targets must be simple names")
356
- var_name = stmt.targets[0].id
357
- value = stmt.value
358
- awaited = False
359
- if isinstance(value, ast.Await):
360
- value = value.value
361
- awaited = True
362
- if isinstance(value, ast.Call):
363
- return _emit_assign_from_call(var_name, value, awaited)
364
- elif isinstance(value, ast.JoinedStr):
365
- return _emit_assign_from_fstring(var_name, value)
366
- elif isinstance(value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
367
- return _emit_assign_from_literal_or_pack(var_name, value)
368
- elif isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
369
- return _emit_assign_from_comp(var_name, value)
370
- elif isinstance(value, ast.Subscript):
371
- return _emit_assign_from_subscript(var_name, value)
397
+ if len(stmt.targets) != 1:
398
+ raise DSLParseError("Assignment must have exactly one target")
399
+ target = stmt.targets[0]
400
+ if isinstance(target, ast.Name):
401
+ var_name = target.id
402
+ value = stmt.value
403
+ awaited = False
404
+ if isinstance(value, ast.Await):
405
+ value = value.value
406
+ awaited = True
407
+ if isinstance(value, ast.Call):
408
+ return _emit_assign_from_call(var_name, value, awaited)
409
+ elif isinstance(value, ast.JoinedStr):
410
+ return _emit_assign_from_fstring(var_name, value)
411
+ elif isinstance(value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
412
+ return _emit_assign_from_literal_or_pack(var_name, value)
413
+ elif isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
414
+ return _emit_assign_from_comp(var_name, value)
415
+ elif isinstance(value, ast.Subscript):
416
+ return _emit_assign_from_subscript(var_name, value)
417
+ else:
418
+ raise DSLParseError("Right hand side must be a call or f-string")
419
+ elif isinstance(target, ast.Subscript):
420
+ return _emit_assign_to_subscript(target, stmt.value)
372
421
  else:
373
- raise DSLParseError("Right hand side must be a call or f-string")
422
+ raise DSLParseError("Assignment targets must be simple names")
374
423
  elif isinstance(stmt, ast.Expr):
375
424
  call = stmt.value
376
425
  awaited = False
@@ -406,6 +455,9 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
406
455
  _emit_expr_call(call, awaited)
407
456
  return None
408
457
  elif isinstance(stmt, ast.Return):
458
+ if loop_depth > 0:
459
+ ssa = _ssa_new("break")
460
+ ops.append({"id": ssa, "op": "CTRL.break", "deps": [], "args": {}})
409
461
  if isinstance(stmt.value, ast.Name):
410
462
  returned_var = _ssa_get(stmt.value.id)
411
463
  elif isinstance(stmt.value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
@@ -503,6 +555,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
503
555
  saved_ctx = context_suffix
504
556
  ctx_counts["loop"] += 1
505
557
  context_suffix = f"loop{ctx_counts['loop']}"
558
+ loop_depth += 1
506
559
  versions, latest = versions_body, latest_body
507
560
  # Predefine loop target variables as items from iterator for dependency resolution
508
561
  def _bind_loop_target(target: ast.AST):
@@ -529,6 +582,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
529
582
  _bind_loop_target(stmt.target)
530
583
  for inner in stmt.body:
531
584
  _parse_stmt(inner)
585
+ loop_depth -= 1
532
586
  versions_body, latest_body = versions, latest
533
587
  versions, latest = saved_versions, saved_latest
534
588
  context_suffix = saved_ctx
@@ -571,9 +625,11 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
571
625
  saved_ctx = context_suffix
572
626
  ctx_counts["while"] += 1
573
627
  context_suffix = f"while{ctx_counts['while']}"
628
+ loop_depth += 1
574
629
  versions, latest = versions_body, latest_body
575
630
  for inner in stmt.body:
576
631
  _parse_stmt(inner)
632
+ loop_depth -= 1
577
633
  versions_body, latest_body = versions, latest
578
634
  versions, latest = saved_versions, saved_latest
579
635
  context_suffix = saved_ctx
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "py2dag"
7
- version = "0.3.3"
7
+ version = "0.3.4"
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
File without changes