py2dag 0.3.3__py3-none-any.whl → 0.3.5__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
@@ -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,29 @@ 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
+ val_id = _emit_value(f"{base.id}_item", value)
347
+
348
+ base_id = _ssa_get(base.id)
349
+ ssa = _ssa_new(base.id)
350
+ ops.append({
351
+ "id": ssa,
352
+ "op": "SET.item",
353
+ "deps": [base_id, val_id],
354
+ "args": {"key": key},
355
+ })
356
+ return ssa
357
+
334
358
  def _emit_cond(node: ast.AST, kind: str = "if") -> str:
335
359
  expr = _stringify(node)
336
360
  deps = [_ssa_get(n) for n in _collect_value_deps(node)]
@@ -338,6 +362,51 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
338
362
  ops.append({"id": ssa, "op": "COND.eval", "deps": deps, "args": {"expr": expr, "kind": kind}})
339
363
  return ssa
340
364
 
365
+ def _emit_assign_from_ifexp(var_name: str, node: ast.IfExp) -> str:
366
+ """Emit operations for an inline ``a if cond else b`` expression."""
367
+ cond_id = _emit_cond(node.test, kind="ifexp")
368
+
369
+ then_start = len(ops)
370
+ then_id = _emit_value(f"{var_name}_then", node.body)
371
+ if len(ops) > then_start:
372
+ first = ops[then_start]
373
+ deps0 = first.get("deps", []) or []
374
+ if cond_id not in deps0:
375
+ first["deps"] = [*deps0, cond_id]
376
+
377
+ else_start = len(ops)
378
+ else_id = _emit_value(f"{var_name}_else", node.orelse)
379
+ if len(ops) > else_start:
380
+ first = ops[else_start]
381
+ deps0 = first.get("deps", []) or []
382
+ if cond_id not in deps0:
383
+ first["deps"] = [*deps0, cond_id]
384
+
385
+ ssa = _ssa_new(var_name)
386
+ ops.append({"id": ssa, "op": "PHI", "deps": [then_id, else_id], "args": {"var": var_name}})
387
+ return ssa
388
+
389
+ def _emit_value(var_name: str, value: ast.AST) -> str:
390
+ awaited = False
391
+ if isinstance(value, ast.Await):
392
+ value = value.value
393
+ awaited = True
394
+ if isinstance(value, ast.Call):
395
+ return _emit_assign_from_call(var_name, value, awaited)
396
+ if isinstance(value, ast.JoinedStr):
397
+ return _emit_assign_from_fstring(var_name, value)
398
+ if isinstance(value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
399
+ return _emit_assign_from_literal_or_pack(var_name, value)
400
+ if isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
401
+ return _emit_assign_from_comp(var_name, value)
402
+ if isinstance(value, ast.Subscript):
403
+ return _emit_assign_from_subscript(var_name, value)
404
+ if isinstance(value, ast.IfExp):
405
+ return _emit_assign_from_ifexp(var_name, value)
406
+ if isinstance(value, ast.Name):
407
+ return _ssa_get(value.id)
408
+ raise DSLParseError("Right hand side must be a call or f-string")
409
+
341
410
  def _emit_iter(node: ast.AST, target_label: Optional[str] = None) -> str:
342
411
  expr = _stringify(node)
343
412
  deps = [_ssa_get(n) for n in _collect_value_deps(node)]
@@ -349,28 +418,18 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
349
418
  return ssa
350
419
 
351
420
  def _parse_stmt(stmt: ast.stmt) -> Optional[str]:
352
- nonlocal returned_var, versions, latest, context_suffix
421
+ nonlocal returned_var, versions, latest, context_suffix, loop_depth
353
422
  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)
423
+ if len(stmt.targets) != 1:
424
+ raise DSLParseError("Assignment must have exactly one target")
425
+ target = stmt.targets[0]
426
+ if isinstance(target, ast.Name):
427
+ var_name = target.id
428
+ return _emit_value(var_name, stmt.value)
429
+ elif isinstance(target, ast.Subscript):
430
+ return _emit_assign_to_subscript(target, stmt.value)
372
431
  else:
373
- raise DSLParseError("Right hand side must be a call or f-string")
432
+ raise DSLParseError("Assignment targets must be simple names")
374
433
  elif isinstance(stmt, ast.Expr):
375
434
  call = stmt.value
376
435
  awaited = False
@@ -406,8 +465,22 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
406
465
  _emit_expr_call(call, awaited)
407
466
  return None
408
467
  elif isinstance(stmt, ast.Return):
468
+ if loop_depth > 0:
469
+ ssa = _ssa_new("break")
470
+ ops.append({"id": ssa, "op": "CTRL.break", "deps": [], "args": {}})
409
471
  if isinstance(stmt.value, ast.Name):
410
- returned_var = _ssa_get(stmt.value.id)
472
+ name = stmt.value.id
473
+ if name in latest:
474
+ returned_var = _ssa_get(name)
475
+ else:
476
+ const_id = _ssa_new("return_value")
477
+ ops.append({
478
+ "id": const_id,
479
+ "op": "CONST.value",
480
+ "deps": [],
481
+ "args": {"value": None},
482
+ })
483
+ returned_var = const_id
411
484
  elif isinstance(stmt.value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
412
485
  lit = _literal(stmt.value)
413
486
  const_id = _ssa_new("return_value")
@@ -503,6 +576,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
503
576
  saved_ctx = context_suffix
504
577
  ctx_counts["loop"] += 1
505
578
  context_suffix = f"loop{ctx_counts['loop']}"
579
+ loop_depth += 1
506
580
  versions, latest = versions_body, latest_body
507
581
  # Predefine loop target variables as items from iterator for dependency resolution
508
582
  def _bind_loop_target(target: ast.AST):
@@ -529,6 +603,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
529
603
  _bind_loop_target(stmt.target)
530
604
  for inner in stmt.body:
531
605
  _parse_stmt(inner)
606
+ loop_depth -= 1
532
607
  versions_body, latest_body = versions, latest
533
608
  versions, latest = saved_versions, saved_latest
534
609
  context_suffix = saved_ctx
@@ -571,9 +646,11 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
571
646
  saved_ctx = context_suffix
572
647
  ctx_counts["while"] += 1
573
648
  context_suffix = f"while{ctx_counts['while']}"
649
+ loop_depth += 1
574
650
  versions, latest = versions_body, latest_body
575
651
  for inner in stmt.body:
576
652
  _parse_stmt(inner)
653
+ loop_depth -= 1
577
654
  versions_body, latest_body = versions, latest
578
655
  versions, latest = saved_versions, saved_latest
579
656
  context_suffix = saved_ctx
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: py2dag
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Convert Python function plans to DAG (JSON, pseudo, optional SVG).
5
5
  License: MIT
6
6
  Author: rvergis
@@ -3,10 +3,10 @@ py2dag/cli.py,sha256=q8ocafpGtkrls1mx9QiFU1wL6KKeeJhT0Oklk6SwHB0,15151
3
3
  py2dag/colors.py,sha256=kVMVWUKJY1uLQFQux5XKiEcofDFpNPQoAEzpMJWs2b0,604
4
4
  py2dag/export_dagre.py,sha256=Og-oVjKzc7bRgeU7rgLHDjiY6wIcNyr2nIYlSAQzKoM,6041
5
5
  py2dag/export_svg.py,sha256=3ZmZqxIUidSbuKIIh5kFqhfVnoqZ6PbEQpMih4AX9xo,3793
6
- py2dag/parser.py,sha256=WN2ZbOZdc05mOr60Q-IDA9TPsTX6tKb_VuDdBEBFeq8,31862
6
+ py2dag/parser.py,sha256=HWmXK7lSmERL6HcP36KIMdNHU9jBWmum8VKz5G2Rp7k,35128
7
7
  py2dag/pseudo.py,sha256=NJK61slyFLtSjhj8gJDJneUInEpBN57_41g8IfHNPWI,922
8
- py2dag-0.3.3.dist-info/LICENSE,sha256=3Qee1EPwej_nusovTbyIQ8LvD2rXHdM0c6LNwk_D8Kc,1067
9
- py2dag-0.3.3.dist-info/METADATA,sha256=Y6H9SC22ONmNes0_2ELcSAOtJlbsEpXKr0lzi2f2zss,3549
10
- py2dag-0.3.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
- py2dag-0.3.3.dist-info/entry_points.txt,sha256=Q0SHexJJ0z1te4AYL1xTZogx5FrxCCE1ZJ5qntkFMZs,42
12
- py2dag-0.3.3.dist-info/RECORD,,
8
+ py2dag-0.3.5.dist-info/LICENSE,sha256=3Qee1EPwej_nusovTbyIQ8LvD2rXHdM0c6LNwk_D8Kc,1067
9
+ py2dag-0.3.5.dist-info/METADATA,sha256=msm0oyCl8_UVfxTVTUlRthioOuSxx3sx8K40FywRLQA,3549
10
+ py2dag-0.3.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
+ py2dag-0.3.5.dist-info/entry_points.txt,sha256=Q0SHexJJ0z1te4AYL1xTZogx5FrxCCE1ZJ5qntkFMZs,42
12
+ py2dag-0.3.5.dist-info/RECORD,,
File without changes