py2dag 0.1.13__py3-none-any.whl → 0.1.15__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
@@ -47,6 +47,25 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
47
47
  settings: Dict[str, Any] = {}
48
48
 
49
49
  returned_var: Optional[str] = None
50
+ # Enforce no-args top-level function signature
51
+ try:
52
+ fargs = getattr(fn, "args") # type: ignore[attr-defined]
53
+ has_params = bool(
54
+ getattr(fargs, "posonlyargs", []) or fargs.args or fargs.vararg or fargs.kwonlyargs or fargs.kwarg
55
+ )
56
+ if has_params:
57
+ raise DSLParseError("Top-level function must not accept parameters")
58
+ except AttributeError:
59
+ # If args not present, ignore
60
+ pass
61
+
62
+ def _collect_name_deps(node: ast.AST) -> List[str]:
63
+ names: List[str] = []
64
+ for n in ast.walk(node):
65
+ if isinstance(n, ast.Name) and isinstance(n.ctx, ast.Load):
66
+ if n.id not in names:
67
+ names.append(n.id)
68
+ return names
50
69
  # type: ignore[attr-defined]
51
70
  for i, stmt in enumerate(fn.body): # type: ignore[attr-defined]
52
71
  if isinstance(stmt, ast.Assign):
@@ -65,8 +84,32 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
65
84
  op_name = _get_call_name(value.func)
66
85
 
67
86
  deps: List[str] = []
87
+
88
+ def _expand_star_name(varname: str) -> List[str]:
89
+ # Try to expand a previously packed list/tuple variable into its element deps
90
+ for prev in reversed(ops):
91
+ if prev.get("id") == varname:
92
+ if prev.get("op") in {"PACK.list", "PACK.tuple"}:
93
+ return list(prev.get("deps", []))
94
+ break
95
+ return [varname]
68
96
  for arg in value.args:
69
- if isinstance(arg, ast.Name):
97
+ if isinstance(arg, ast.Starred):
98
+ star_val = arg.value
99
+ if isinstance(star_val, ast.Name):
100
+ if star_val.id not in defined:
101
+ raise DSLParseError(f"Undefined dependency: {star_val.id}")
102
+ deps.extend(_expand_star_name(star_val.id))
103
+ elif isinstance(star_val, (ast.List, ast.Tuple)):
104
+ for elt in star_val.elts:
105
+ if not isinstance(elt, ast.Name):
106
+ raise DSLParseError("Starred list/tuple elements must be names")
107
+ if elt.id not in defined:
108
+ raise DSLParseError(f"Undefined dependency: {elt.id}")
109
+ deps.append(elt.id)
110
+ else:
111
+ raise DSLParseError("*args must be a name or list/tuple of names")
112
+ elif isinstance(arg, ast.Name):
70
113
  if arg.id not in defined:
71
114
  raise DSLParseError(f"Undefined dependency: {arg.id}")
72
115
  deps.append(arg.id)
@@ -83,15 +126,28 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
83
126
  kwargs: Dict[str, Any] = {}
84
127
  for kw in value.keywords:
85
128
  if kw.arg is None:
86
- raise DSLParseError("**kwargs are not allowed")
87
- # Support variable-name keyword args as dependencies; literals remain in args
88
- if isinstance(kw.value, ast.Name):
89
- name = kw.value.id
90
- if name not in defined:
91
- raise DSLParseError(f"Undefined dependency: {name}")
92
- deps.append(name)
129
+ # **kwargs support: allow dict literal merge, or variable name as dep
130
+ v = kw.value
131
+ if isinstance(v, ast.Dict):
132
+ # Merge literal kwargs
133
+ lit = _literal(v)
134
+ for k, val in lit.items():
135
+ kwargs[str(k)] = val
136
+ elif isinstance(v, ast.Name):
137
+ if v.id not in defined:
138
+ raise DSLParseError(f"Undefined dependency: {v.id}")
139
+ deps.append(v.id)
140
+ else:
141
+ raise DSLParseError("**kwargs must be a dict literal or a variable name")
93
142
  else:
94
- kwargs[kw.arg] = _literal(kw.value)
143
+ # Support variable-name keyword args as dependencies; literals remain in args
144
+ if isinstance(kw.value, ast.Name):
145
+ name = kw.value.id
146
+ if name not in defined:
147
+ raise DSLParseError(f"Undefined dependency: {name}")
148
+ deps.append(name)
149
+ else:
150
+ kwargs[kw.arg] = _literal(kw.value)
95
151
 
96
152
  ops.append({"id": var_name, "op": op_name, "deps": deps, "args": kwargs})
97
153
  elif isinstance(value, ast.JoinedStr):
@@ -117,13 +173,52 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
117
173
  "args": {"template": template},
118
174
  })
119
175
  elif isinstance(value, (ast.Constant, ast.List, ast.Tuple, ast.Dict)):
120
- # Allow assigning JSON-serialisable literals directly
121
- lit = _literal(value)
176
+ # Allow assigning literals; also support packing lists/tuples of names
177
+ try:
178
+ lit = _literal(value)
179
+ ops.append({
180
+ "id": var_name,
181
+ "op": "CONST.value",
182
+ "deps": [],
183
+ "args": {"value": lit},
184
+ })
185
+ except DSLParseError:
186
+ if isinstance(value, (ast.List, ast.Tuple)):
187
+ elts = value.elts
188
+ names: List[str] = []
189
+ for elt in elts:
190
+ if not isinstance(elt, ast.Name):
191
+ raise DSLParseError("Only names allowed in non-literal list/tuple assignment")
192
+ if elt.id not in defined:
193
+ raise DSLParseError(f"Undefined dependency: {elt.id}")
194
+ names.append(elt.id)
195
+ kind = "list" if isinstance(value, ast.List) else "tuple"
196
+ ops.append({
197
+ "id": var_name,
198
+ "op": f"PACK.{kind}",
199
+ "deps": names,
200
+ "args": {},
201
+ })
202
+ else:
203
+ raise
204
+ elif isinstance(value, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
205
+ # Basic comprehension support: collect name deps and emit a generic comp op
206
+ name_deps = [n for n in _collect_name_deps(value) if n in defined]
207
+ # Ensure no undefined names used
208
+ for n in name_deps:
209
+ if n not in defined:
210
+ raise DSLParseError(f"Undefined dependency: {n}")
211
+ kind = (
212
+ "listcomp" if isinstance(value, ast.ListComp) else
213
+ "setcomp" if isinstance(value, ast.SetComp) else
214
+ "dictcomp" if isinstance(value, ast.DictComp) else
215
+ "genexpr"
216
+ )
122
217
  ops.append({
123
218
  "id": var_name,
124
- "op": "CONST.value",
125
- "deps": [],
126
- "args": {"value": lit},
219
+ "op": f"COMP.{kind}",
220
+ "deps": name_deps,
221
+ "args": {},
127
222
  })
128
223
  else:
129
224
  raise DSLParseError("Right hand side must be a call or f-string")
@@ -186,7 +281,7 @@ def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
186
281
  returned_var = const_id
187
282
  else:
188
283
  raise DSLParseError("return must return a variable name or literal")
189
- elif isinstance(stmt, (ast.For, ast.AsyncFor, ast.While, ast.If)):
284
+ elif isinstance(stmt, (ast.For, ast.AsyncFor, ast.While, ast.If, ast.Match)):
190
285
  # Ignore control flow blocks; only top-level linear statements are modeled
191
286
  continue
192
287
  elif isinstance(stmt, (ast.Pass,)):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: py2dag
3
- Version: 0.1.13
3
+ Version: 0.1.15
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=S244wUxBMuM9qXD8bklaslkMp3rcbBGWvMhwWVBzBF0,3487
4
4
  py2dag/export_svg.py,sha256=YyjqOuj8GhUTDWP70SKnnSWAKI1PvJwyOhHLwB29uNM,1812
5
- py2dag/parser.py,sha256=AdC76fKiLojxrc7Tk20CHgS7U0ofQwBYecMHQycc3PQ,11507
5
+ py2dag/parser.py,sha256=6YPM1MU2FgVDkElK0hxR-PX7NeiQwTTSJvjMkSABF2s,16856
6
6
  py2dag/pseudo.py,sha256=NJK61slyFLtSjhj8gJDJneUInEpBN57_41g8IfHNPWI,922
7
- py2dag-0.1.13.dist-info/LICENSE,sha256=3Qee1EPwej_nusovTbyIQ8LvD2rXHdM0c6LNwk_D8Kc,1067
8
- py2dag-0.1.13.dist-info/METADATA,sha256=gG64U7iwhT-lWu84Tu9c9TFTID6jIBHHI184tGXpkqY,3550
9
- py2dag-0.1.13.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
- py2dag-0.1.13.dist-info/entry_points.txt,sha256=Q0SHexJJ0z1te4AYL1xTZogx5FrxCCE1ZJ5qntkFMZs,42
11
- py2dag-0.1.13.dist-info/RECORD,,
7
+ py2dag-0.1.15.dist-info/LICENSE,sha256=3Qee1EPwej_nusovTbyIQ8LvD2rXHdM0c6LNwk_D8Kc,1067
8
+ py2dag-0.1.15.dist-info/METADATA,sha256=kxE0fmpJMm0xvuglSG2fz9w8E1fnu8vufQGd49YeQT0,3550
9
+ py2dag-0.1.15.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
+ py2dag-0.1.15.dist-info/entry_points.txt,sha256=Q0SHexJJ0z1te4AYL1xTZogx5FrxCCE1ZJ5qntkFMZs,42
11
+ py2dag-0.1.15.dist-info/RECORD,,