py2dag 0.1.4__tar.gz → 0.1.6__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.
- {py2dag-0.1.4 → py2dag-0.1.6}/PKG-INFO +8 -7
- {py2dag-0.1.4 → py2dag-0.1.6}/README.md +7 -6
- {py2dag-0.1.4 → py2dag-0.1.6}/py2dag/cli.py +1 -1
- py2dag-0.1.6/py2dag/parser.py +211 -0
- {py2dag-0.1.4 → py2dag-0.1.6}/pyproject.toml +1 -1
- py2dag-0.1.4/py2dag/parser.py +0 -173
- {py2dag-0.1.4 → py2dag-0.1.6}/LICENSE +0 -0
- {py2dag-0.1.4 → py2dag-0.1.6}/py2dag/__init__.py +0 -0
- {py2dag-0.1.4 → py2dag-0.1.6}/py2dag/export_dagre.py +0 -0
- {py2dag-0.1.4 → py2dag-0.1.6}/py2dag/export_svg.py +0 -0
- {py2dag-0.1.4 → py2dag-0.1.6}/py2dag/pseudo.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: py2dag
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.6
|
4
4
|
Summary: Convert Python function plans to DAG (JSON, pseudo, optional SVG).
|
5
5
|
License: MIT
|
6
6
|
Author: rvergis
|
@@ -76,6 +76,8 @@ poetry run python cli.py path/to/your_file.py --html
|
|
76
76
|
|
77
77
|
This generates `plan.json`, `plan.pseudo`, and if `--html` is used, `plan.html`.
|
78
78
|
|
79
|
+
- Function name: By default the tool auto-detects a suitable function in the file. To target a specific function, pass `--func NAME`.
|
80
|
+
|
79
81
|
### Offline HTML (no internet)
|
80
82
|
|
81
83
|
`plan.html` references d3 and dagre-d3 from CDNs. To view graphs fully offline, download those files and place them next to `plan.html`, then edit the two `<script>` tags in `py2dag/export_dagre.py` to point at your local copies.
|
@@ -111,18 +113,17 @@ This repo includes a GitHub Actions workflow that publishes to PyPI when you pus
|
|
111
113
|
|
112
114
|
1) In GitHub repo settings, add a repository secret named `PYPI_API_TOKEN` with your PyPI token (format: `pypi-***`).
|
113
115
|
|
114
|
-
2) Bump patch version,
|
116
|
+
2) Bump patch version, commit, and tag locally; then push tags to trigger the workflow:
|
115
117
|
|
116
118
|
```
|
117
|
-
make
|
119
|
+
make patch # bumps version, commits, and tags vX.Y.Z
|
120
|
+
make release # pushes tags to GitHub (triggers publish)
|
118
121
|
```
|
119
122
|
|
120
|
-
Or
|
123
|
+
Or push tags manually:
|
121
124
|
|
122
125
|
```
|
123
|
-
|
124
|
-
make tag
|
125
|
-
make push-tags
|
126
|
+
git push --tags
|
126
127
|
```
|
127
128
|
|
128
129
|
You can also install from the local build with `pip install dist/*.whl`.
|
@@ -57,6 +57,8 @@ poetry run python cli.py path/to/your_file.py --html
|
|
57
57
|
|
58
58
|
This generates `plan.json`, `plan.pseudo`, and if `--html` is used, `plan.html`.
|
59
59
|
|
60
|
+
- Function name: By default the tool auto-detects a suitable function in the file. To target a specific function, pass `--func NAME`.
|
61
|
+
|
60
62
|
### Offline HTML (no internet)
|
61
63
|
|
62
64
|
`plan.html` references d3 and dagre-d3 from CDNs. To view graphs fully offline, download those files and place them next to `plan.html`, then edit the two `<script>` tags in `py2dag/export_dagre.py` to point at your local copies.
|
@@ -92,18 +94,17 @@ This repo includes a GitHub Actions workflow that publishes to PyPI when you pus
|
|
92
94
|
|
93
95
|
1) In GitHub repo settings, add a repository secret named `PYPI_API_TOKEN` with your PyPI token (format: `pypi-***`).
|
94
96
|
|
95
|
-
2) Bump patch version,
|
97
|
+
2) Bump patch version, commit, and tag locally; then push tags to trigger the workflow:
|
96
98
|
|
97
99
|
```
|
98
|
-
make
|
100
|
+
make patch # bumps version, commits, and tags vX.Y.Z
|
101
|
+
make release # pushes tags to GitHub (triggers publish)
|
99
102
|
```
|
100
103
|
|
101
|
-
Or
|
104
|
+
Or push tags manually:
|
102
105
|
|
103
106
|
```
|
104
|
-
|
105
|
-
make tag
|
106
|
-
make push-tags
|
107
|
+
git push --tags
|
107
108
|
```
|
108
109
|
|
109
110
|
You can also install from the local build with `pip install dist/*.whl`.
|
@@ -9,7 +9,7 @@ from . import export_svg
|
|
9
9
|
def main() -> None:
|
10
10
|
ap = argparse.ArgumentParser(description="Convert Python function plan to DAG")
|
11
11
|
ap.add_argument("file", help="Python file containing the plan function")
|
12
|
-
ap.add_argument("--func", default=
|
12
|
+
ap.add_argument("--func", default=None, help="Function name to parse (auto-detect if omitted)")
|
13
13
|
ap.add_argument("--svg", action="store_true", help="Also export plan.svg via Graphviz (requires dot)")
|
14
14
|
ap.add_argument("--html", action="store_true", help="Also export plan.html via Dagre (no system deps)")
|
15
15
|
args = ap.parse_args()
|
@@ -0,0 +1,211 @@
|
|
1
|
+
import ast
|
2
|
+
import json
|
3
|
+
import re
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
|
+
|
6
|
+
VALID_NAME_RE = re.compile(r'^[a-z_][a-z0-9_]{0,63}$')
|
7
|
+
|
8
|
+
|
9
|
+
class DSLParseError(Exception):
|
10
|
+
"""Raised when the mini-DSL constraints are violated."""
|
11
|
+
|
12
|
+
|
13
|
+
def _literal(node: ast.AST) -> Any:
|
14
|
+
"""Return a Python literal from an AST node or raise DSLParseError."""
|
15
|
+
if isinstance(node, ast.Constant):
|
16
|
+
return node.value
|
17
|
+
if isinstance(node, (ast.List, ast.Tuple)):
|
18
|
+
return [_literal(elt) for elt in node.elts]
|
19
|
+
if isinstance(node, ast.Dict):
|
20
|
+
return {_literal(k): _literal(v) for k, v in zip(node.keys, node.values)}
|
21
|
+
raise DSLParseError("Keyword argument values must be JSON-serialisable literals")
|
22
|
+
|
23
|
+
|
24
|
+
def _get_call_name(func: ast.AST) -> str:
|
25
|
+
if isinstance(func, ast.Name):
|
26
|
+
return func.id
|
27
|
+
if isinstance(func, ast.Attribute):
|
28
|
+
parts: List[str] = []
|
29
|
+
while isinstance(func, ast.Attribute):
|
30
|
+
parts.append(func.attr)
|
31
|
+
func = func.value
|
32
|
+
if isinstance(func, ast.Name):
|
33
|
+
parts.append(func.id)
|
34
|
+
return ".".join(reversed(parts))
|
35
|
+
raise DSLParseError("Only simple or attribute names are allowed for operations")
|
36
|
+
|
37
|
+
|
38
|
+
def parse(source: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
39
|
+
if len(source) > 20_000:
|
40
|
+
raise DSLParseError("Source too large")
|
41
|
+
module = ast.parse(source)
|
42
|
+
|
43
|
+
def _parse_fn(fn: ast.AST) -> Dict[str, Any]:
|
44
|
+
defined: set[str] = set()
|
45
|
+
ops: List[Dict[str, Any]] = []
|
46
|
+
outputs: List[Dict[str, str]] = []
|
47
|
+
settings: Dict[str, Any] = {}
|
48
|
+
|
49
|
+
returned_var: Optional[str] = None
|
50
|
+
# type: ignore[attr-defined]
|
51
|
+
for i, stmt in enumerate(fn.body): # type: ignore[attr-defined]
|
52
|
+
if isinstance(stmt, ast.Assign):
|
53
|
+
if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name):
|
54
|
+
raise DSLParseError("Assignment targets must be simple names")
|
55
|
+
var_name = stmt.targets[0].id
|
56
|
+
if not VALID_NAME_RE.match(var_name):
|
57
|
+
raise DSLParseError(f"Invalid variable name: {var_name}")
|
58
|
+
if var_name in defined:
|
59
|
+
raise DSLParseError(f"Duplicate variable name: {var_name}")
|
60
|
+
|
61
|
+
value = stmt.value
|
62
|
+
if isinstance(value, ast.Await):
|
63
|
+
value = value.value
|
64
|
+
if isinstance(value, ast.Call):
|
65
|
+
op_name = _get_call_name(value.func)
|
66
|
+
|
67
|
+
deps: List[str] = []
|
68
|
+
for arg in value.args:
|
69
|
+
if not isinstance(arg, ast.Name):
|
70
|
+
raise DSLParseError("Positional args must be variable names")
|
71
|
+
if arg.id not in defined:
|
72
|
+
raise DSLParseError(f"Undefined dependency: {arg.id}")
|
73
|
+
deps.append(arg.id)
|
74
|
+
|
75
|
+
kwargs: Dict[str, Any] = {}
|
76
|
+
for kw in value.keywords:
|
77
|
+
if kw.arg is None:
|
78
|
+
raise DSLParseError("**kwargs are not allowed")
|
79
|
+
kwargs[kw.arg] = _literal(kw.value)
|
80
|
+
|
81
|
+
ops.append({"id": var_name, "op": op_name, "deps": deps, "args": kwargs})
|
82
|
+
elif isinstance(value, ast.JoinedStr):
|
83
|
+
# Minimal f-string support: only variable placeholders
|
84
|
+
deps: List[str] = []
|
85
|
+
parts: List[str] = []
|
86
|
+
for item in value.values:
|
87
|
+
if isinstance(item, ast.Constant) and isinstance(item.value, str):
|
88
|
+
parts.append(item.value)
|
89
|
+
elif isinstance(item, ast.FormattedValue) and isinstance(item.value, ast.Name):
|
90
|
+
name = item.value.id
|
91
|
+
if name not in defined:
|
92
|
+
raise DSLParseError(f"Undefined dependency: {name}")
|
93
|
+
deps.append(name)
|
94
|
+
parts.append("{" + str(len(deps) - 1) + "}")
|
95
|
+
else:
|
96
|
+
raise DSLParseError("f-strings may only contain variable names")
|
97
|
+
template = "".join(parts)
|
98
|
+
ops.append({
|
99
|
+
"id": var_name,
|
100
|
+
"op": "TEXT.format",
|
101
|
+
"deps": deps,
|
102
|
+
"args": {"template": template},
|
103
|
+
})
|
104
|
+
else:
|
105
|
+
raise DSLParseError("Right hand side must be a call or f-string")
|
106
|
+
defined.add(var_name)
|
107
|
+
|
108
|
+
elif isinstance(stmt, ast.Expr):
|
109
|
+
call = stmt.value
|
110
|
+
if isinstance(call, ast.Await):
|
111
|
+
call = call.value
|
112
|
+
if not isinstance(call, ast.Call):
|
113
|
+
raise DSLParseError("Only call expressions allowed at top level")
|
114
|
+
name = _get_call_name(call.func)
|
115
|
+
if name == "settings":
|
116
|
+
for kw in call.keywords:
|
117
|
+
if kw.arg is None:
|
118
|
+
raise DSLParseError("settings does not accept **kwargs")
|
119
|
+
settings[kw.arg] = _literal(kw.value)
|
120
|
+
if call.args:
|
121
|
+
raise DSLParseError("settings only accepts keyword literals")
|
122
|
+
elif name == "output":
|
123
|
+
if len(call.args) != 1 or not isinstance(call.args[0], ast.Name):
|
124
|
+
raise DSLParseError("output requires a single variable name argument")
|
125
|
+
var = call.args[0].id
|
126
|
+
if var not in defined:
|
127
|
+
raise DSLParseError(f"Undefined output variable: {var}")
|
128
|
+
filename = None
|
129
|
+
for kw in call.keywords:
|
130
|
+
if kw.arg in {"as", "as_"}:
|
131
|
+
filename = _literal(kw.value)
|
132
|
+
else:
|
133
|
+
raise DSLParseError("output only accepts 'as' keyword")
|
134
|
+
if filename is None or not isinstance(filename, str):
|
135
|
+
raise DSLParseError("output requires as=\"filename\"")
|
136
|
+
outputs.append({"from": var, "as": filename})
|
137
|
+
else:
|
138
|
+
raise DSLParseError("Only settings() and output() calls allowed as expressions")
|
139
|
+
elif isinstance(stmt, ast.Return):
|
140
|
+
if i != len(fn.body) - 1: # type: ignore[index]
|
141
|
+
raise DSLParseError("return must be the last statement")
|
142
|
+
if isinstance(stmt.value, ast.Name):
|
143
|
+
var = stmt.value.id
|
144
|
+
if var not in defined:
|
145
|
+
raise DSLParseError(f"Undefined return variable: {var}")
|
146
|
+
returned_var = var
|
147
|
+
elif isinstance(stmt.value, ast.Constant):
|
148
|
+
# Support returning a literal (e.g., "DONE"): synthesize a const op
|
149
|
+
lit = stmt.value.value
|
150
|
+
const_id_base = "return_value"
|
151
|
+
const_id = const_id_base
|
152
|
+
n = 1
|
153
|
+
while const_id in defined:
|
154
|
+
const_id = f"{const_id_base}_{n}"
|
155
|
+
n += 1
|
156
|
+
ops.append({
|
157
|
+
"id": const_id,
|
158
|
+
"op": "CONST.value",
|
159
|
+
"deps": [],
|
160
|
+
"args": {"value": lit},
|
161
|
+
})
|
162
|
+
returned_var = const_id
|
163
|
+
else:
|
164
|
+
raise DSLParseError("return must return a variable name or literal")
|
165
|
+
else:
|
166
|
+
raise DSLParseError("Only assignments, expression calls, and a final return are allowed in function body")
|
167
|
+
|
168
|
+
if not outputs:
|
169
|
+
if returned_var is not None:
|
170
|
+
outputs.append({"from": returned_var, "as": "return"})
|
171
|
+
else:
|
172
|
+
raise DSLParseError("At least one output() call required")
|
173
|
+
if len(ops) > 200:
|
174
|
+
raise DSLParseError("Too many operations")
|
175
|
+
|
176
|
+
# Include the parsed function name for visibility/debugging
|
177
|
+
fn_name = getattr(fn, "name", None) # type: ignore[attr-defined]
|
178
|
+
plan: Dict[str, Any] = {"version": 1, "function": fn_name, "ops": ops, "outputs": outputs}
|
179
|
+
if settings:
|
180
|
+
plan["settings"] = settings
|
181
|
+
return plan
|
182
|
+
|
183
|
+
# If a specific function name is provided, use it; otherwise try to auto-detect
|
184
|
+
if function_name is not None:
|
185
|
+
fn: Optional[ast.AST] = None
|
186
|
+
for node in module.body:
|
187
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == function_name:
|
188
|
+
fn = node
|
189
|
+
break
|
190
|
+
if fn is None:
|
191
|
+
raise DSLParseError(f"Function {function_name!r} not found")
|
192
|
+
return _parse_fn(fn)
|
193
|
+
else:
|
194
|
+
last_err: Optional[Exception] = None
|
195
|
+
for node in module.body:
|
196
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
197
|
+
try:
|
198
|
+
return _parse_fn(node)
|
199
|
+
except DSLParseError as e:
|
200
|
+
last_err = e
|
201
|
+
continue
|
202
|
+
# If we got here, either there are no functions or none matched the DSL
|
203
|
+
if last_err is not None:
|
204
|
+
raise DSLParseError("No suitable function matched the DSL; specify --func to disambiguate") from last_err
|
205
|
+
raise DSLParseError("No function definitions found in source")
|
206
|
+
|
207
|
+
|
208
|
+
def parse_file(filename: str, function_name: Optional[str] = None) -> Dict[str, Any]:
|
209
|
+
with open(filename, "r", encoding="utf-8") as f:
|
210
|
+
src = f.read()
|
211
|
+
return parse(src, function_name=function_name)
|
py2dag-0.1.4/py2dag/parser.py
DELETED
@@ -1,173 +0,0 @@
|
|
1
|
-
import ast
|
2
|
-
import json
|
3
|
-
import re
|
4
|
-
from typing import Any, Dict, List, Optional
|
5
|
-
|
6
|
-
VALID_NAME_RE = re.compile(r'^[a-z_][a-z0-9_]{0,63}$')
|
7
|
-
|
8
|
-
|
9
|
-
class DSLParseError(Exception):
|
10
|
-
"""Raised when the mini-DSL constraints are violated."""
|
11
|
-
|
12
|
-
|
13
|
-
def _literal(node: ast.AST) -> Any:
|
14
|
-
"""Return a Python literal from an AST node or raise DSLParseError."""
|
15
|
-
if isinstance(node, ast.Constant):
|
16
|
-
return node.value
|
17
|
-
if isinstance(node, (ast.List, ast.Tuple)):
|
18
|
-
return [_literal(elt) for elt in node.elts]
|
19
|
-
if isinstance(node, ast.Dict):
|
20
|
-
return {_literal(k): _literal(v) for k, v in zip(node.keys, node.values)}
|
21
|
-
raise DSLParseError("Keyword argument values must be JSON-serialisable literals")
|
22
|
-
|
23
|
-
|
24
|
-
def _get_call_name(func: ast.AST) -> str:
|
25
|
-
if isinstance(func, ast.Name):
|
26
|
-
return func.id
|
27
|
-
if isinstance(func, ast.Attribute):
|
28
|
-
parts: List[str] = []
|
29
|
-
while isinstance(func, ast.Attribute):
|
30
|
-
parts.append(func.attr)
|
31
|
-
func = func.value
|
32
|
-
if isinstance(func, ast.Name):
|
33
|
-
parts.append(func.id)
|
34
|
-
return ".".join(reversed(parts))
|
35
|
-
raise DSLParseError("Only simple or attribute names are allowed for operations")
|
36
|
-
|
37
|
-
|
38
|
-
def parse(source: str, function_name: str = "plan") -> Dict[str, Any]:
|
39
|
-
if len(source) > 20_000:
|
40
|
-
raise DSLParseError("Source too large")
|
41
|
-
module = ast.parse(source)
|
42
|
-
fn = None
|
43
|
-
for node in module.body:
|
44
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == function_name:
|
45
|
-
fn = node
|
46
|
-
break
|
47
|
-
if fn is None:
|
48
|
-
raise DSLParseError(f"Function {function_name!r} not found")
|
49
|
-
|
50
|
-
defined: set[str] = set()
|
51
|
-
ops: List[Dict[str, Any]] = []
|
52
|
-
outputs: List[Dict[str, str]] = []
|
53
|
-
settings: Dict[str, Any] = {}
|
54
|
-
|
55
|
-
returned_var: Optional[str] = None
|
56
|
-
for i, stmt in enumerate(fn.body):
|
57
|
-
if isinstance(stmt, ast.Assign):
|
58
|
-
if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name):
|
59
|
-
raise DSLParseError("Assignment targets must be simple names")
|
60
|
-
var_name = stmt.targets[0].id
|
61
|
-
if not VALID_NAME_RE.match(var_name):
|
62
|
-
raise DSLParseError(f"Invalid variable name: {var_name}")
|
63
|
-
if var_name in defined:
|
64
|
-
raise DSLParseError(f"Duplicate variable name: {var_name}")
|
65
|
-
|
66
|
-
value = stmt.value
|
67
|
-
if isinstance(value, ast.Await):
|
68
|
-
value = value.value
|
69
|
-
if isinstance(value, ast.Call):
|
70
|
-
op_name = _get_call_name(value.func)
|
71
|
-
|
72
|
-
deps: List[str] = []
|
73
|
-
for arg in value.args:
|
74
|
-
if not isinstance(arg, ast.Name):
|
75
|
-
raise DSLParseError("Positional args must be variable names")
|
76
|
-
if arg.id not in defined:
|
77
|
-
raise DSLParseError(f"Undefined dependency: {arg.id}")
|
78
|
-
deps.append(arg.id)
|
79
|
-
|
80
|
-
kwargs: Dict[str, Any] = {}
|
81
|
-
for kw in value.keywords:
|
82
|
-
if kw.arg is None:
|
83
|
-
raise DSLParseError("**kwargs are not allowed")
|
84
|
-
kwargs[kw.arg] = _literal(kw.value)
|
85
|
-
|
86
|
-
ops.append({"id": var_name, "op": op_name, "deps": deps, "args": kwargs})
|
87
|
-
elif isinstance(value, ast.JoinedStr):
|
88
|
-
# Minimal f-string support: only variable placeholders
|
89
|
-
deps: List[str] = []
|
90
|
-
parts: List[str] = []
|
91
|
-
for item in value.values:
|
92
|
-
if isinstance(item, ast.Constant) and isinstance(item.value, str):
|
93
|
-
parts.append(item.value)
|
94
|
-
elif isinstance(item, ast.FormattedValue) and isinstance(item.value, ast.Name):
|
95
|
-
name = item.value.id
|
96
|
-
if name not in defined:
|
97
|
-
raise DSLParseError(f"Undefined dependency: {name}")
|
98
|
-
deps.append(name)
|
99
|
-
parts.append("{" + str(len(deps) - 1) + "}")
|
100
|
-
else:
|
101
|
-
raise DSLParseError("f-strings may only contain variable names")
|
102
|
-
template = "".join(parts)
|
103
|
-
ops.append({
|
104
|
-
"id": var_name,
|
105
|
-
"op": "TEXT.format",
|
106
|
-
"deps": deps,
|
107
|
-
"args": {"template": template},
|
108
|
-
})
|
109
|
-
else:
|
110
|
-
raise DSLParseError("Right hand side must be a call or f-string")
|
111
|
-
defined.add(var_name)
|
112
|
-
|
113
|
-
elif isinstance(stmt, ast.Expr):
|
114
|
-
call = stmt.value
|
115
|
-
if isinstance(call, ast.Await):
|
116
|
-
call = call.value
|
117
|
-
if not isinstance(call, ast.Call):
|
118
|
-
raise DSLParseError("Only call expressions allowed at top level")
|
119
|
-
name = _get_call_name(call.func)
|
120
|
-
if name == "settings":
|
121
|
-
for kw in call.keywords:
|
122
|
-
if kw.arg is None:
|
123
|
-
raise DSLParseError("settings does not accept **kwargs")
|
124
|
-
settings[kw.arg] = _literal(kw.value)
|
125
|
-
if call.args:
|
126
|
-
raise DSLParseError("settings only accepts keyword literals")
|
127
|
-
elif name == "output":
|
128
|
-
if len(call.args) != 1 or not isinstance(call.args[0], ast.Name):
|
129
|
-
raise DSLParseError("output requires a single variable name argument")
|
130
|
-
var = call.args[0].id
|
131
|
-
if var not in defined:
|
132
|
-
raise DSLParseError(f"Undefined output variable: {var}")
|
133
|
-
filename = None
|
134
|
-
for kw in call.keywords:
|
135
|
-
if kw.arg in {"as", "as_"}:
|
136
|
-
filename = _literal(kw.value)
|
137
|
-
else:
|
138
|
-
raise DSLParseError("output only accepts 'as' keyword")
|
139
|
-
if filename is None or not isinstance(filename, str):
|
140
|
-
raise DSLParseError("output requires as=\"filename\"")
|
141
|
-
outputs.append({"from": var, "as": filename})
|
142
|
-
else:
|
143
|
-
raise DSLParseError("Only settings() and output() calls allowed as expressions")
|
144
|
-
elif isinstance(stmt, ast.Return):
|
145
|
-
if i != len(fn.body) - 1:
|
146
|
-
raise DSLParseError("return must be the last statement")
|
147
|
-
if not isinstance(stmt.value, ast.Name):
|
148
|
-
raise DSLParseError("return must return a variable name")
|
149
|
-
var = stmt.value.id
|
150
|
-
if var not in defined:
|
151
|
-
raise DSLParseError(f"Undefined return variable: {var}")
|
152
|
-
returned_var = var
|
153
|
-
else:
|
154
|
-
raise DSLParseError("Only assignments, expression calls, and a final return are allowed in function body")
|
155
|
-
|
156
|
-
if not outputs:
|
157
|
-
if returned_var is not None:
|
158
|
-
outputs.append({"from": returned_var, "as": "return"})
|
159
|
-
else:
|
160
|
-
raise DSLParseError("At least one output() call required")
|
161
|
-
if len(ops) > 200:
|
162
|
-
raise DSLParseError("Too many operations")
|
163
|
-
|
164
|
-
plan: Dict[str, Any] = {"version": 1, "ops": ops, "outputs": outputs}
|
165
|
-
if settings:
|
166
|
-
plan["settings"] = settings
|
167
|
-
return plan
|
168
|
-
|
169
|
-
|
170
|
-
def parse_file(filename: str, function_name: str = "plan") -> Dict[str, Any]:
|
171
|
-
with open(filename, "r", encoding="utf-8") as f:
|
172
|
-
src = f.read()
|
173
|
-
return parse(src, function_name=function_name)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|