cs-object 0.1.0__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.
- cs_object-0.1.0.dist-info/METADATA +103 -0
- cs_object-0.1.0.dist-info/RECORD +22 -0
- cs_object-0.1.0.dist-info/WHEEL +5 -0
- cs_object-0.1.0.dist-info/licenses/LICENSE +202 -0
- cs_object-0.1.0.dist-info/licenses/NOTICE +4 -0
- cs_object-0.1.0.dist-info/top_level.txt +1 -0
- cso_python/__init__.py +38 -0
- cso_python/__main__.py +45 -0
- cso_python/annotations.py +81 -0
- cso_python/authoring.py +128 -0
- cso_python/bindings.py +168 -0
- cso_python/definitions.py +414 -0
- cso_python/describe.py +149 -0
- cso_python/execution.py +553 -0
- cso_python/exporter.py +912 -0
- cso_python/function_calls.py +58 -0
- cso_python/glyphs.py +81 -0
- cso_python/handles.py +120 -0
- cso_python/notation.py +693 -0
- cso_python/planner.py +1201 -0
- cso_python/py.typed +0 -0
- cso_python/source.py +650 -0
cso_python/bindings.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Generate runtime handles and type stubs from static calculation definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import keyword
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .definitions import Definitions
|
|
13
|
+
from .source import Capture, SourceError, authoring_name
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate(directory: Path, *, check: bool = False) -> dict:
|
|
17
|
+
root = directory.resolve(strict=True)
|
|
18
|
+
sources = sorted(root.rglob("*.cso.py"))
|
|
19
|
+
if not sources:
|
|
20
|
+
raise SourceError("MISSING_SOURCE", "No .cso.py calculations found")
|
|
21
|
+
if not root.is_dir():
|
|
22
|
+
raise SourceError("INVALID_DEPENDENCY_PATH", "Bindings require a directory")
|
|
23
|
+
output = root / "_cso_bindings"
|
|
24
|
+
if output.is_symlink() or any(path.is_symlink() for path in output.rglob("*")):
|
|
25
|
+
raise SourceError(
|
|
26
|
+
"INVALID_DEPENDENCY_PATH",
|
|
27
|
+
"Generated bindings cannot contain symbolic links",
|
|
28
|
+
)
|
|
29
|
+
files: dict[Path, str] = {output / "__init__.py": "", output / "py.typed": ""}
|
|
30
|
+
for source in sources:
|
|
31
|
+
relative = source.relative_to(root)
|
|
32
|
+
parts = [*relative.parts[:-1], relative.name[:-7]]
|
|
33
|
+
if any(not part.isidentifier() or keyword.iskeyword(part) for part in parts):
|
|
34
|
+
raise SourceError(
|
|
35
|
+
"INVALID_BINDING_NAME",
|
|
36
|
+
f"Binding module needs identifier path components: {relative}",
|
|
37
|
+
)
|
|
38
|
+
capture = Capture(source)
|
|
39
|
+
capture.root = root
|
|
40
|
+
capture.for_bindings = True
|
|
41
|
+
module = capture.load(source)
|
|
42
|
+
definitions = Definitions(capture)
|
|
43
|
+
runtime = [
|
|
44
|
+
"# Generated by cso bindings. Do not edit.\n",
|
|
45
|
+
"from cso_python import load_calculation\n\n",
|
|
46
|
+
]
|
|
47
|
+
stub = [
|
|
48
|
+
"# Generated by cso bindings. Do not edit.\n",
|
|
49
|
+
"from typing import TypedDict\n\n",
|
|
50
|
+
]
|
|
51
|
+
target = output.joinpath(*parts).with_suffix(".py")
|
|
52
|
+
parent = target.parent
|
|
53
|
+
while parent != output:
|
|
54
|
+
files[parent / "__init__.py"] = ""
|
|
55
|
+
parent = parent.parent
|
|
56
|
+
names = set(module.functions)
|
|
57
|
+
reserved = {"TypedDict", "load_calculation"} | {
|
|
58
|
+
f"_{name}_Outputs" for name in names
|
|
59
|
+
}
|
|
60
|
+
if names & reserved:
|
|
61
|
+
raise SourceError(
|
|
62
|
+
"BINDING_OUTPUT_CONFLICT",
|
|
63
|
+
f"Calculation names collide with binding helpers: {sorted(names & reserved)}",
|
|
64
|
+
)
|
|
65
|
+
for name, fn in module.functions.items():
|
|
66
|
+
if not any(
|
|
67
|
+
authoring_name(d.func) == "calculation" for d in fn.decorator_list
|
|
68
|
+
):
|
|
69
|
+
continue
|
|
70
|
+
definition = definitions.get(source, name)
|
|
71
|
+
path = os.path.relpath(source, target.parent)
|
|
72
|
+
runtime.append(
|
|
73
|
+
f"{name} = load_calculation({path!r}, function={name!r}, fingerprint={definition.fingerprint!r})\n"
|
|
74
|
+
)
|
|
75
|
+
output_type = f"_{name}_Outputs"
|
|
76
|
+
fields = ", ".join(
|
|
77
|
+
f"{key!r}: {spec.annotation.numeric_type}"
|
|
78
|
+
for key, spec in definition.outputs.items()
|
|
79
|
+
)
|
|
80
|
+
stub.append(f"{output_type} = TypedDict({output_type!r}, {{{fields}}})\n")
|
|
81
|
+
parameters = ", ".join(
|
|
82
|
+
f"{key}: {spec.declared.numeric_type}"
|
|
83
|
+
+ (f" = {spec.default_value!r}" if spec.default is not None else "")
|
|
84
|
+
for key, spec in definition.parameters.items()
|
|
85
|
+
)
|
|
86
|
+
stub.append(
|
|
87
|
+
f"def {name}({('*, ' + parameters) if parameters else ''}) -> {output_type}: ...\n\n"
|
|
88
|
+
)
|
|
89
|
+
if target in files or target.with_suffix(".pyi") in files:
|
|
90
|
+
raise SourceError(
|
|
91
|
+
"BINDING_OUTPUT_CONFLICT", f"Colliding binding module {target}"
|
|
92
|
+
)
|
|
93
|
+
if target.name == "__init__.py":
|
|
94
|
+
raise SourceError(
|
|
95
|
+
"BINDING_OUTPUT_CONFLICT",
|
|
96
|
+
"__init__.cso.py collides with a generated package",
|
|
97
|
+
)
|
|
98
|
+
files[target] = "".join(runtime)
|
|
99
|
+
files[target.with_suffix(".pyi")] = "".join(stub)
|
|
100
|
+
expected = set(files)
|
|
101
|
+
existing = (
|
|
102
|
+
{p for p in output.rglob("*") if p.is_file() and "__pycache__" not in p.parts}
|
|
103
|
+
if output.exists()
|
|
104
|
+
else set()
|
|
105
|
+
)
|
|
106
|
+
stale = [
|
|
107
|
+
str(p.relative_to(root))
|
|
108
|
+
for p in sorted(expected | existing)
|
|
109
|
+
if p not in files or not p.exists() or p.read_text() != files[p]
|
|
110
|
+
]
|
|
111
|
+
if not check:
|
|
112
|
+
# Check ownership before replacing or removing an existing generated file.
|
|
113
|
+
for path in existing:
|
|
114
|
+
if not (
|
|
115
|
+
path.name in ("__init__.py", "py.typed") and not path.read_bytes()
|
|
116
|
+
) and not path.read_text().startswith("# Generated by cso bindings."):
|
|
117
|
+
raise SourceError(
|
|
118
|
+
"BINDING_OUTPUT_CONFLICT",
|
|
119
|
+
f"Refusing to overwrite authored file {path}",
|
|
120
|
+
)
|
|
121
|
+
for path, content in files.items():
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
124
|
+
dir=path.parent, prefix=".cso-bindings-"
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
with os.fdopen(descriptor, "w") as stream:
|
|
128
|
+
stream.write(content)
|
|
129
|
+
os.replace(temporary, path)
|
|
130
|
+
finally:
|
|
131
|
+
if os.path.exists(temporary):
|
|
132
|
+
os.unlink(temporary)
|
|
133
|
+
for path in existing - expected:
|
|
134
|
+
path.unlink()
|
|
135
|
+
return {
|
|
136
|
+
"ok": not check or not stale,
|
|
137
|
+
"command": "bindings",
|
|
138
|
+
"check": check,
|
|
139
|
+
"directory": str(root),
|
|
140
|
+
"files": [str(p.relative_to(root)) for p in sorted(files)],
|
|
141
|
+
"stale": stale,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def bindings_from_argv(argv: list[str]) -> int:
|
|
146
|
+
parser = argparse.ArgumentParser(
|
|
147
|
+
prog="python -m cso_python bindings", description=__doc__
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument("directory", type=Path)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--check",
|
|
152
|
+
action="store_true",
|
|
153
|
+
help="Report stale bindings without writing files",
|
|
154
|
+
)
|
|
155
|
+
args = parser.parse_args(argv)
|
|
156
|
+
try:
|
|
157
|
+
result = generate(args.directory, check=args.check)
|
|
158
|
+
except (SourceError, OSError, ValueError) as error:
|
|
159
|
+
result = {
|
|
160
|
+
"ok": False,
|
|
161
|
+
"diagnostics": [
|
|
162
|
+
error.diagnostic
|
|
163
|
+
if isinstance(error, SourceError)
|
|
164
|
+
else {"code": "BINDINGS_FAILED", "message": str(error)}
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
print(json.dumps(result, ensure_ascii=False, allow_nan=False))
|
|
168
|
+
return 0 if result["ok"] else 1
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Static public interfaces shared by generation and invocation planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import hashlib
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .annotations import Annotation, annotation, symbol_annotation
|
|
11
|
+
from .source import (
|
|
12
|
+
Capture,
|
|
13
|
+
CapturedModule,
|
|
14
|
+
Json,
|
|
15
|
+
SourceError,
|
|
16
|
+
authoring_name,
|
|
17
|
+
compact,
|
|
18
|
+
literal,
|
|
19
|
+
number,
|
|
20
|
+
span,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ParameterDefinition:
|
|
26
|
+
"""Keep declared input behavior separate from legacy given-row documentation."""
|
|
27
|
+
|
|
28
|
+
node: ast.arg
|
|
29
|
+
declared: Annotation
|
|
30
|
+
documented: Annotation
|
|
31
|
+
default: ast.expr | None
|
|
32
|
+
default_value: int | float | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class OutputDefinition:
|
|
37
|
+
annotation: Annotation
|
|
38
|
+
selection: ast.expr
|
|
39
|
+
parameter_source: str | None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Definition:
|
|
44
|
+
module: CapturedModule
|
|
45
|
+
function: ast.FunctionDef
|
|
46
|
+
parameters: dict[str, ParameterDefinition]
|
|
47
|
+
outputs: dict[str, OutputDefinition]
|
|
48
|
+
fingerprint: str
|
|
49
|
+
given_parameters: frozenset[str]
|
|
50
|
+
legacy_return: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Definitions:
|
|
54
|
+
def __init__(self, capture: Capture):
|
|
55
|
+
self.capture = capture
|
|
56
|
+
self.cache: dict[tuple[Path, str], Definition] = {}
|
|
57
|
+
self.visiting: set[tuple[Path, str]] = set()
|
|
58
|
+
|
|
59
|
+
def get(self, path: Path, function: str) -> Definition:
|
|
60
|
+
key = (path.resolve(), function)
|
|
61
|
+
if key in self.cache:
|
|
62
|
+
return self.cache[key]
|
|
63
|
+
if key in self.visiting:
|
|
64
|
+
raise SourceError("DEPENDENCY_CYCLE", f"Recursive calculation {function}")
|
|
65
|
+
self.visiting.add(key)
|
|
66
|
+
try:
|
|
67
|
+
definition = self._read(self.capture.load(path), function)
|
|
68
|
+
self.cache[key] = definition
|
|
69
|
+
return definition
|
|
70
|
+
finally:
|
|
71
|
+
self.visiting.remove(key)
|
|
72
|
+
|
|
73
|
+
def _read(self, module: CapturedModule, function: str) -> Definition:
|
|
74
|
+
fn = module.functions.get(function)
|
|
75
|
+
if fn is None:
|
|
76
|
+
raise SourceError("MISSING_FUNCTION", f"Missing calculation {function}")
|
|
77
|
+
if fn.args.posonlyargs or fn.args.vararg or fn.args.kwarg:
|
|
78
|
+
raise SourceError(
|
|
79
|
+
"UNSUPPORTED_SIGNATURE",
|
|
80
|
+
"Use named positional or keyword-only parameters",
|
|
81
|
+
location=span(module.id, fn),
|
|
82
|
+
)
|
|
83
|
+
parameter_nodes = [*fn.args.args, *fn.args.kwonlyargs]
|
|
84
|
+
nodes = {a.arg: a for a in parameter_nodes}
|
|
85
|
+
if len(nodes) != len(parameter_nodes):
|
|
86
|
+
raise SourceError(
|
|
87
|
+
"UNSUPPORTED_SIGNATURE",
|
|
88
|
+
"Parameter names must be unique",
|
|
89
|
+
location=span(module.id, fn),
|
|
90
|
+
)
|
|
91
|
+
if nodes.keys() & module.imported or any(
|
|
92
|
+
name.startswith("__cso_") for name in nodes
|
|
93
|
+
):
|
|
94
|
+
raise SourceError(
|
|
95
|
+
"UNSUPPORTED_SIGNATURE",
|
|
96
|
+
"Parameters cannot shadow imported authoring helpers or math names",
|
|
97
|
+
location=span(module.id, fn),
|
|
98
|
+
)
|
|
99
|
+
defaults = (
|
|
100
|
+
dict(
|
|
101
|
+
zip(
|
|
102
|
+
[a.arg for a in fn.args.args][-len(fn.args.defaults) :],
|
|
103
|
+
fn.args.defaults,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if fn.args.defaults
|
|
107
|
+
else {}
|
|
108
|
+
)
|
|
109
|
+
defaults.update(
|
|
110
|
+
{
|
|
111
|
+
a.arg: d
|
|
112
|
+
for a, d in zip(fn.args.kwonlyargs, fn.args.kw_defaults)
|
|
113
|
+
if d is not None
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
parameters = {}
|
|
117
|
+
for name, node in nodes.items():
|
|
118
|
+
if node.annotation is None:
|
|
119
|
+
raise SourceError(
|
|
120
|
+
"UNSUPPORTED_SIGNATURE",
|
|
121
|
+
"Parameters need numeric annotations",
|
|
122
|
+
location=span(module.id, node),
|
|
123
|
+
)
|
|
124
|
+
declared = annotation(node.annotation, module.id, module.aliases)
|
|
125
|
+
default = defaults.get(name)
|
|
126
|
+
parameters[name] = ParameterDefinition(
|
|
127
|
+
node=node,
|
|
128
|
+
declared=declared,
|
|
129
|
+
documented=declared,
|
|
130
|
+
default=default,
|
|
131
|
+
default_value=(
|
|
132
|
+
number(literal(default, module.id), span(module.id, default))
|
|
133
|
+
if default is not None
|
|
134
|
+
else None
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
legacy_signature = all(p.declared.metadata is None for p in parameters.values())
|
|
138
|
+
documented_names: list[str] = []
|
|
139
|
+
known = {name: p.declared for name, p in parameters.items()}
|
|
140
|
+
children: dict[str, dict[str, Annotation]] = {}
|
|
141
|
+
forwarded: dict[str, dict[str, str]] = {}
|
|
142
|
+
assigned: set[str] = set()
|
|
143
|
+
given_parameters: set[str] = set()
|
|
144
|
+
outputs: dict[str, OutputDefinition] = {}
|
|
145
|
+
returned = False
|
|
146
|
+
legacy_return = False
|
|
147
|
+
|
|
148
|
+
def reference(expr: ast.expr) -> Annotation:
|
|
149
|
+
if isinstance(expr, ast.Name) and expr.id in known:
|
|
150
|
+
return known[expr.id]
|
|
151
|
+
if (
|
|
152
|
+
isinstance(expr, ast.Subscript)
|
|
153
|
+
and isinstance(expr.value, ast.Name)
|
|
154
|
+
and expr.value.id in children
|
|
155
|
+
and isinstance(expr.slice, ast.Constant)
|
|
156
|
+
and isinstance(expr.slice.value, str)
|
|
157
|
+
):
|
|
158
|
+
output = children[expr.value.id].get(expr.slice.value)
|
|
159
|
+
if output is not None:
|
|
160
|
+
return output
|
|
161
|
+
raise SourceError(
|
|
162
|
+
"MISSING_OUTPUT",
|
|
163
|
+
"Public outputs must select documented quantities",
|
|
164
|
+
location=span(module.id, expr),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def parameter_source(expr: ast.expr) -> str | None:
|
|
168
|
+
if (
|
|
169
|
+
isinstance(expr, ast.Name)
|
|
170
|
+
and expr.id in parameters
|
|
171
|
+
and expr.id not in assigned
|
|
172
|
+
):
|
|
173
|
+
return expr.id
|
|
174
|
+
if (
|
|
175
|
+
isinstance(expr, ast.Subscript)
|
|
176
|
+
and isinstance(expr.value, ast.Name)
|
|
177
|
+
and isinstance(expr.slice, ast.Constant)
|
|
178
|
+
and isinstance(expr.slice.value, str)
|
|
179
|
+
):
|
|
180
|
+
return forwarded.get(expr.value.id, {}).get(expr.slice.value)
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def dependency(path: Path, function: str, call: ast.Assign) -> Definition:
|
|
184
|
+
try:
|
|
185
|
+
return self.get(path, function)
|
|
186
|
+
except SourceError as error:
|
|
187
|
+
error.diagnostic.setdefault("callChain", []).insert(
|
|
188
|
+
0, span(module.id, call)
|
|
189
|
+
)
|
|
190
|
+
raise
|
|
191
|
+
|
|
192
|
+
def bind_child(
|
|
193
|
+
name: str, child: Definition, arguments: dict[str, ast.expr]
|
|
194
|
+
) -> None:
|
|
195
|
+
children[name] = {
|
|
196
|
+
key: output.annotation for key, output in child.outputs.items()
|
|
197
|
+
}
|
|
198
|
+
forwarded[name] = {}
|
|
199
|
+
for output, selection in child.outputs.items():
|
|
200
|
+
if selection.parameter_source is None:
|
|
201
|
+
continue
|
|
202
|
+
argument = arguments.get(selection.parameter_source)
|
|
203
|
+
if argument is None or isinstance(
|
|
204
|
+
argument, (ast.Constant, ast.UnaryOp)
|
|
205
|
+
):
|
|
206
|
+
continue
|
|
207
|
+
children[name][output] = reference(argument)
|
|
208
|
+
parent_parameter = parameter_source(argument)
|
|
209
|
+
if parent_parameter:
|
|
210
|
+
forwarded[name][output] = parent_parameter
|
|
211
|
+
|
|
212
|
+
def statements(body: list[ast.stmt], *, grouped: bool = False) -> None:
|
|
213
|
+
nonlocal returned, legacy_return
|
|
214
|
+
for index, node in enumerate(body):
|
|
215
|
+
if isinstance(node, ast.AnnAssign) and isinstance(
|
|
216
|
+
node.target, ast.Name
|
|
217
|
+
):
|
|
218
|
+
if node.target.id in assigned:
|
|
219
|
+
raise SourceError(
|
|
220
|
+
"DUPLICATE_IDENTITY",
|
|
221
|
+
"Symbols need unique simple names",
|
|
222
|
+
location=span(module.id, node),
|
|
223
|
+
)
|
|
224
|
+
assigned.add(node.target.id)
|
|
225
|
+
documented_names.append(node.target.id)
|
|
226
|
+
declared = symbol_annotation(node, module.id, module.aliases)
|
|
227
|
+
known[node.target.id] = declared
|
|
228
|
+
if (
|
|
229
|
+
isinstance(node.value, ast.Call)
|
|
230
|
+
and authoring_name(node.value.func) == "given"
|
|
231
|
+
and len(node.value.args) == 1
|
|
232
|
+
and isinstance(node.value.args[0], ast.Name)
|
|
233
|
+
):
|
|
234
|
+
parameter = node.value.args[0].id
|
|
235
|
+
if parameter in parameters:
|
|
236
|
+
original = parameters[parameter]
|
|
237
|
+
if (
|
|
238
|
+
original.declared.metadata is not None
|
|
239
|
+
or parameter in given_parameters
|
|
240
|
+
):
|
|
241
|
+
raise SourceError(
|
|
242
|
+
"AMBIGUOUS_METADATA",
|
|
243
|
+
"A parameter must have one metadata declaration",
|
|
244
|
+
location=span(module.id, original.node),
|
|
245
|
+
)
|
|
246
|
+
given_parameters.add(parameter)
|
|
247
|
+
parameters[parameter] = replace(
|
|
248
|
+
original, documented=declared
|
|
249
|
+
)
|
|
250
|
+
elif (
|
|
251
|
+
isinstance(node, ast.Assign)
|
|
252
|
+
and isinstance(node.value, ast.Call)
|
|
253
|
+
and len(node.targets) == 1
|
|
254
|
+
and isinstance(node.targets[0], ast.Name)
|
|
255
|
+
):
|
|
256
|
+
call = node.value
|
|
257
|
+
name = authoring_name(call.func)
|
|
258
|
+
if name in module.handles:
|
|
259
|
+
handle = module.handles[name]
|
|
260
|
+
child = dependency(handle["path"], handle["function"], node)
|
|
261
|
+
if (
|
|
262
|
+
handle["fingerprint"] is not None
|
|
263
|
+
and handle["fingerprint"] != child.fingerprint
|
|
264
|
+
):
|
|
265
|
+
raise SourceError(
|
|
266
|
+
"STALE_BINDINGS",
|
|
267
|
+
"Run cso bindings to refresh the changed public interface",
|
|
268
|
+
location=span(module.id, call),
|
|
269
|
+
)
|
|
270
|
+
bind_child(
|
|
271
|
+
node.targets[0].id,
|
|
272
|
+
child,
|
|
273
|
+
{
|
|
274
|
+
kw.arg: kw.value
|
|
275
|
+
for kw in call.keywords
|
|
276
|
+
if kw.arg is not None
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
elif name == "calculation_call":
|
|
280
|
+
kw = {k.arg: k.value for k in call.keywords}
|
|
281
|
+
if call.args and "function" in kw:
|
|
282
|
+
path = literal(call.args[0], module.id)
|
|
283
|
+
function_name = literal(kw["function"], module.id)
|
|
284
|
+
if (
|
|
285
|
+
not isinstance(path, str)
|
|
286
|
+
or not path
|
|
287
|
+
or Path(path).is_absolute()
|
|
288
|
+
or "\\" in path
|
|
289
|
+
or ":" in path
|
|
290
|
+
or not isinstance(function_name, str)
|
|
291
|
+
or not function_name.isidentifier()
|
|
292
|
+
):
|
|
293
|
+
raise SourceError(
|
|
294
|
+
"INVALID_CALL",
|
|
295
|
+
"Use a static relative path and function name",
|
|
296
|
+
location=span(module.id, call),
|
|
297
|
+
)
|
|
298
|
+
child = dependency(
|
|
299
|
+
module.path.parent / path, function_name, node
|
|
300
|
+
)
|
|
301
|
+
inputs = kw.get("inputs")
|
|
302
|
+
if isinstance(inputs, ast.Dict):
|
|
303
|
+
arguments = {}
|
|
304
|
+
for key, value in zip(inputs.keys, inputs.values):
|
|
305
|
+
if (
|
|
306
|
+
not isinstance(key, ast.Constant)
|
|
307
|
+
or not isinstance(key.value, str)
|
|
308
|
+
or key.value in arguments
|
|
309
|
+
):
|
|
310
|
+
raise SourceError(
|
|
311
|
+
"INVALID_CALL_INPUTS",
|
|
312
|
+
"Duplicate call input or nonliteral key",
|
|
313
|
+
location=span(module.id, inputs),
|
|
314
|
+
related=[
|
|
315
|
+
span(child.module.id, child.function)
|
|
316
|
+
],
|
|
317
|
+
)
|
|
318
|
+
arguments[key.value] = value
|
|
319
|
+
bind_child(node.targets[0].id, child, arguments)
|
|
320
|
+
elif isinstance(node, ast.With):
|
|
321
|
+
statements(node.body, grouped=True)
|
|
322
|
+
elif isinstance(node, ast.Return):
|
|
323
|
+
if grouped:
|
|
324
|
+
raise SourceError(
|
|
325
|
+
"UNSUPPORTED_SYNTAX",
|
|
326
|
+
"Document groups cannot contain returns",
|
|
327
|
+
location=span(module.id, node),
|
|
328
|
+
)
|
|
329
|
+
if index != len(body) - 1:
|
|
330
|
+
raise SourceError(
|
|
331
|
+
"UNSUPPORTED_SYNTAX",
|
|
332
|
+
"Statements after return are unsupported",
|
|
333
|
+
location=span(module.id, body[index + 1]),
|
|
334
|
+
)
|
|
335
|
+
if not isinstance(node.value, ast.Dict):
|
|
336
|
+
raise SourceError(
|
|
337
|
+
"INVALID_RETURN_KEYS",
|
|
338
|
+
"Return a literal-key result dictionary",
|
|
339
|
+
location=span(module.id, node),
|
|
340
|
+
)
|
|
341
|
+
keys = [
|
|
342
|
+
key.value
|
|
343
|
+
if isinstance(key, ast.Constant) and isinstance(key.value, str)
|
|
344
|
+
else None
|
|
345
|
+
for key in node.value.keys
|
|
346
|
+
]
|
|
347
|
+
if any(key is None or not key for key in keys) or len(
|
|
348
|
+
set(keys)
|
|
349
|
+
) != len(keys):
|
|
350
|
+
raise SourceError(
|
|
351
|
+
"INVALID_RETURN_KEYS",
|
|
352
|
+
"Return unique nonempty literal output names",
|
|
353
|
+
location=span(module.id, node),
|
|
354
|
+
)
|
|
355
|
+
# Preserve source order, including assignments inside document groups.
|
|
356
|
+
# Complete legacy maps keep incorrect runtime returns independently observable.
|
|
357
|
+
legacy_return = legacy_signature and keys == documented_names
|
|
358
|
+
for key, value in zip(keys, node.value.values):
|
|
359
|
+
outputs[key] = OutputDefinition(
|
|
360
|
+
annotation=known[key]
|
|
361
|
+
if legacy_return
|
|
362
|
+
else reference(value),
|
|
363
|
+
selection=value,
|
|
364
|
+
parameter_source=parameter_source(value),
|
|
365
|
+
)
|
|
366
|
+
returned = True
|
|
367
|
+
|
|
368
|
+
statements(fn.body)
|
|
369
|
+
if not returned:
|
|
370
|
+
raise SourceError(
|
|
371
|
+
"INVALID_RETURN_KEYS",
|
|
372
|
+
"Calculation needs one final return dictionary",
|
|
373
|
+
location=span(module.id, fn),
|
|
374
|
+
)
|
|
375
|
+
interface: Json = {
|
|
376
|
+
"function": function,
|
|
377
|
+
"parameters": [
|
|
378
|
+
{
|
|
379
|
+
"name": name,
|
|
380
|
+
"type": spec.declared.numeric_type,
|
|
381
|
+
**(
|
|
382
|
+
{"documentedType": spec.documented.numeric_type}
|
|
383
|
+
if spec.documented.numeric_type != spec.declared.numeric_type
|
|
384
|
+
else {}
|
|
385
|
+
),
|
|
386
|
+
"metadata": spec.documented.metadata,
|
|
387
|
+
**(
|
|
388
|
+
{"default": spec.default_value}
|
|
389
|
+
if spec.default is not None
|
|
390
|
+
else {}
|
|
391
|
+
),
|
|
392
|
+
"keywordOnly": name in {a.arg for a in fn.args.kwonlyargs},
|
|
393
|
+
}
|
|
394
|
+
for name, spec in parameters.items()
|
|
395
|
+
],
|
|
396
|
+
"outputs": [
|
|
397
|
+
{
|
|
398
|
+
"name": name,
|
|
399
|
+
"selection": ast.dump(spec.selection, include_attributes=False),
|
|
400
|
+
"type": spec.annotation.numeric_type,
|
|
401
|
+
"metadata": spec.annotation.metadata,
|
|
402
|
+
}
|
|
403
|
+
for name, spec in outputs.items()
|
|
404
|
+
],
|
|
405
|
+
}
|
|
406
|
+
return Definition(
|
|
407
|
+
module=module,
|
|
408
|
+
function=fn,
|
|
409
|
+
parameters=parameters,
|
|
410
|
+
outputs=outputs,
|
|
411
|
+
fingerprint=hashlib.sha256(compact(interface).encode()).hexdigest(),
|
|
412
|
+
given_parameters=frozenset(given_parameters),
|
|
413
|
+
legacy_return=legacy_return,
|
|
414
|
+
)
|