@styx-api/core 0.1.0
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.
- package/dist/index.cjs +7947 -0
- package/dist/index.d.cts +1143 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1143 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +7877 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/src/backend/backend.ts +95 -0
- package/src/backend/boutiques/boutiques.ts +1049 -0
- package/src/backend/boutiques/index.ts +1 -0
- package/src/backend/code-builder.ts +49 -0
- package/src/backend/collect-field-info.ts +50 -0
- package/src/backend/collect-named-types.ts +103 -0
- package/src/backend/collect-output-fields.ts +222 -0
- package/src/backend/find-doc.ts +38 -0
- package/src/backend/find-struct-node.ts +66 -0
- package/src/backend/index.ts +39 -0
- package/src/backend/python/arg-builder.ts +454 -0
- package/src/backend/python/emit.ts +638 -0
- package/src/backend/python/index.ts +9 -0
- package/src/backend/python/outputs-emit.ts +430 -0
- package/src/backend/python/packaging.ts +173 -0
- package/src/backend/python/python.ts +558 -0
- package/src/backend/python/snippet.ts +84 -0
- package/src/backend/python/typemap.ts +131 -0
- package/src/backend/python/types.ts +8 -0
- package/src/backend/python/validate-emit.ts +356 -0
- package/src/backend/resolve-field-binding.ts +41 -0
- package/src/backend/resolve-output-tokens.ts +80 -0
- package/src/backend/schema/index.ts +2 -0
- package/src/backend/schema/jsonschema.ts +303 -0
- package/src/backend/scope.ts +50 -0
- package/src/backend/sig-entries.ts +97 -0
- package/src/backend/snippet-core.ts +185 -0
- package/src/backend/string-case.ts +30 -0
- package/src/backend/styxdefs-compat.ts +21 -0
- package/src/backend/type-keys.ts +52 -0
- package/src/backend/typescript/arg-builder.ts +420 -0
- package/src/backend/typescript/emit.ts +450 -0
- package/src/backend/typescript/index.ts +10 -0
- package/src/backend/typescript/outputs-emit.ts +389 -0
- package/src/backend/typescript/packaging.ts +130 -0
- package/src/backend/typescript/snippet.ts +60 -0
- package/src/backend/typescript/typemap.ts +47 -0
- package/src/backend/typescript/types.ts +8 -0
- package/src/backend/typescript/typescript.ts +507 -0
- package/src/backend/typescript/validate-emit.ts +341 -0
- package/src/backend/union-variants.ts +42 -0
- package/src/backend/validate-walk.ts +111 -0
- package/src/bindings/binding.ts +77 -0
- package/src/bindings/format.ts +176 -0
- package/src/bindings/index.ts +16 -0
- package/src/bindings/output-gate.ts +50 -0
- package/src/bindings/resolved-output.ts +56 -0
- package/src/bindings/types.ts +16 -0
- package/src/frontend/argdump/index.ts +1 -0
- package/src/frontend/argdump/parser.ts +914 -0
- package/src/frontend/boutiques/destruct-template.ts +50 -0
- package/src/frontend/boutiques/index.ts +1 -0
- package/src/frontend/boutiques/parser.ts +676 -0
- package/src/frontend/boutiques/split-command.ts +69 -0
- package/src/frontend/detect-format.ts +42 -0
- package/src/frontend/frontend.ts +31 -0
- package/src/frontend/index.ts +9 -0
- package/src/frontend/workbench/index.ts +1 -0
- package/src/frontend/workbench/parser.ts +351 -0
- package/src/index.ts +41 -0
- package/src/ir/builders.ts +69 -0
- package/src/ir/format.ts +157 -0
- package/src/ir/index.ts +32 -0
- package/src/ir/meta.ts +91 -0
- package/src/ir/node.ts +95 -0
- package/src/ir/passes/canonicalize.ts +108 -0
- package/src/ir/passes/flatten.ts +73 -0
- package/src/ir/passes/index.ts +7 -0
- package/src/ir/passes/pass.ts +86 -0
- package/src/ir/passes/pipeline.ts +21 -0
- package/src/ir/passes/remove-empty.ts +76 -0
- package/src/ir/passes/simplify.ts +179 -0
- package/src/ir/types.ts +15 -0
- package/src/manifest/context.ts +36 -0
- package/src/manifest/index.ts +3 -0
- package/src/manifest/types.ts +15 -0
- package/src/solver/assign-access.ts +218 -0
- package/src/solver/index.ts +4 -0
- package/src/solver/resolve-outputs.ts +233 -0
- package/src/solver/solver.ts +319 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import type { BoundType } from "../../bindings/index.js";
|
|
2
|
+
import type { AppMeta } from "../../ir/index.js";
|
|
3
|
+
import type { CodegenContext, PackageMeta, ProjectMeta } from "../../manifest/index.js";
|
|
4
|
+
import type { AppEntrypoint, Backend, EmitResult, EmittedApp, EmittedPackage } from "../backend.js";
|
|
5
|
+
import type { SigEntry } from "../sig-entries.js";
|
|
6
|
+
import type { NamedType } from "./types.js";
|
|
7
|
+
import {
|
|
8
|
+
generateRequirementsTxt,
|
|
9
|
+
generateRootPyproject,
|
|
10
|
+
generateRootReadme,
|
|
11
|
+
generateSubPyproject,
|
|
12
|
+
generateSubReadme,
|
|
13
|
+
pyDistName,
|
|
14
|
+
} from "./packaging.js";
|
|
15
|
+
import { CodeBuilder } from "../code-builder.js";
|
|
16
|
+
import { Scope } from "../scope.js";
|
|
17
|
+
import { pascalCase, screamingSnakeCase, snakeCase } from "../string-case.js";
|
|
18
|
+
import { buildSigEntries } from "../sig-entries.js";
|
|
19
|
+
import {
|
|
20
|
+
emitBuildCargs,
|
|
21
|
+
emitImports,
|
|
22
|
+
emitKwargWrapper,
|
|
23
|
+
emitMetadata,
|
|
24
|
+
emitParamsFactory,
|
|
25
|
+
emitTypeDeclarations,
|
|
26
|
+
emitWrapperFunction,
|
|
27
|
+
pyScrubIdent,
|
|
28
|
+
pySigOptions,
|
|
29
|
+
} from "./emit.js";
|
|
30
|
+
import { collectFieldInfo } from "./types.js";
|
|
31
|
+
import { emitValidate } from "./validate-emit.js";
|
|
32
|
+
import {
|
|
33
|
+
emitBuildOutputs,
|
|
34
|
+
emitOutputsClass,
|
|
35
|
+
emitStripExtensionsHelper,
|
|
36
|
+
needsStripExtensionsHelper,
|
|
37
|
+
streamFieldIds,
|
|
38
|
+
} from "./outputs-emit.js";
|
|
39
|
+
import { mapType } from "./typemap.js";
|
|
40
|
+
import { collectNamedTypes, resolveTypeName, structKey, unionKey } from "./types.js";
|
|
41
|
+
|
|
42
|
+
// Python reserved words + commonly-shadowed built-ins. Used to avoid collisions
|
|
43
|
+
// when generating identifiers. Includes keywords, common stdlib builtins, and
|
|
44
|
+
// the styxdefs symbols we emit/import.
|
|
45
|
+
const PY_RESERVED: ReadonlySet<string> = new Set([
|
|
46
|
+
"False",
|
|
47
|
+
"None",
|
|
48
|
+
"True",
|
|
49
|
+
"and",
|
|
50
|
+
"as",
|
|
51
|
+
"assert",
|
|
52
|
+
"async",
|
|
53
|
+
"await",
|
|
54
|
+
"break",
|
|
55
|
+
"class",
|
|
56
|
+
"continue",
|
|
57
|
+
"def",
|
|
58
|
+
"del",
|
|
59
|
+
"elif",
|
|
60
|
+
"else",
|
|
61
|
+
"except",
|
|
62
|
+
"finally",
|
|
63
|
+
"for",
|
|
64
|
+
"from",
|
|
65
|
+
"global",
|
|
66
|
+
"if",
|
|
67
|
+
"import",
|
|
68
|
+
"in",
|
|
69
|
+
"is",
|
|
70
|
+
"lambda",
|
|
71
|
+
"nonlocal",
|
|
72
|
+
"not",
|
|
73
|
+
"or",
|
|
74
|
+
"pass",
|
|
75
|
+
"raise",
|
|
76
|
+
"return",
|
|
77
|
+
"try",
|
|
78
|
+
"while",
|
|
79
|
+
"with",
|
|
80
|
+
"yield",
|
|
81
|
+
// Common builtins to avoid shadowing.
|
|
82
|
+
"list",
|
|
83
|
+
"dict",
|
|
84
|
+
"tuple",
|
|
85
|
+
"set",
|
|
86
|
+
"int",
|
|
87
|
+
"float",
|
|
88
|
+
"str",
|
|
89
|
+
"bool",
|
|
90
|
+
"type",
|
|
91
|
+
"print",
|
|
92
|
+
"open",
|
|
93
|
+
"input",
|
|
94
|
+
"range",
|
|
95
|
+
"len",
|
|
96
|
+
"id",
|
|
97
|
+
"object",
|
|
98
|
+
"Exception",
|
|
99
|
+
"ValueError",
|
|
100
|
+
"TypeError",
|
|
101
|
+
// styxdefs symbols we emit/import.
|
|
102
|
+
"Runner",
|
|
103
|
+
"Execution",
|
|
104
|
+
"Metadata",
|
|
105
|
+
"InputPathType",
|
|
106
|
+
"OutputPathType",
|
|
107
|
+
"get_global_runner",
|
|
108
|
+
"dataclasses",
|
|
109
|
+
"typing",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Per-tool public symbol names. With the flat `fsl/bet.py` layout each tool
|
|
114
|
+
* file emits these names directly - there is no internal/public alias split.
|
|
115
|
+
*/
|
|
116
|
+
export interface PublicNames {
|
|
117
|
+
params: string;
|
|
118
|
+
outputs: string;
|
|
119
|
+
metadata: string;
|
|
120
|
+
cargs: string;
|
|
121
|
+
outputsFn: string;
|
|
122
|
+
paramsFn: string;
|
|
123
|
+
execute: string;
|
|
124
|
+
validate: string;
|
|
125
|
+
wrapper: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Public-name scheme used by the Python backend. Exported so the CLI and tests can use it. */
|
|
129
|
+
export function computePublicNames(appId: string | undefined): PublicNames {
|
|
130
|
+
if (!appId) {
|
|
131
|
+
return {
|
|
132
|
+
params: "Params",
|
|
133
|
+
outputs: "Outputs",
|
|
134
|
+
metadata: "METADATA",
|
|
135
|
+
cargs: "cargs",
|
|
136
|
+
outputsFn: "outputs",
|
|
137
|
+
paramsFn: "build_params",
|
|
138
|
+
execute: "execute",
|
|
139
|
+
validate: "validate",
|
|
140
|
+
wrapper: "run",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Pre-scrub digit-leading ids (e.g. `3dPFM`) so all derived case forms
|
|
144
|
+
// produce valid Python identifiers in a consistent case (matches v1's
|
|
145
|
+
// `V_3D_PFM_METADATA` / `v_3d_pfm` style instead of mixed `v_3D_PFM`).
|
|
146
|
+
const id = /^[0-9]/.test(appId) ? "v_" + appId : appId;
|
|
147
|
+
return {
|
|
148
|
+
params: pascalCase(id),
|
|
149
|
+
outputs: pascalCase(id) + "Outputs",
|
|
150
|
+
metadata: screamingSnakeCase(id) + "_METADATA",
|
|
151
|
+
cargs: snakeCase(id) + "_cargs",
|
|
152
|
+
outputsFn: snakeCase(id) + "_outputs",
|
|
153
|
+
paramsFn: snakeCase(id) + "_params",
|
|
154
|
+
execute: snakeCase(id) + "_execute",
|
|
155
|
+
validate: snakeCase(id) + "_validate",
|
|
156
|
+
wrapper: snakeCase(id),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The fully-derived naming/typing model for one tool's Python emission. Computed
|
|
162
|
+
* once by `buildEmitModel` so the file emitter and the call-site snippet renderer
|
|
163
|
+
* share the exact same public names, scrubbed kwarg names, and root typing - the
|
|
164
|
+
* snippet must match the function the generated code actually exposes.
|
|
165
|
+
*/
|
|
166
|
+
export interface PyEmitModel {
|
|
167
|
+
appId: string | undefined;
|
|
168
|
+
pkg: string | undefined;
|
|
169
|
+
names: {
|
|
170
|
+
params: string;
|
|
171
|
+
outputs: string;
|
|
172
|
+
metadata: string;
|
|
173
|
+
cargs: string;
|
|
174
|
+
outputsFn: string;
|
|
175
|
+
paramsFn: string;
|
|
176
|
+
execute: string;
|
|
177
|
+
validate: string;
|
|
178
|
+
wrapper: string;
|
|
179
|
+
};
|
|
180
|
+
rootType: BoundType;
|
|
181
|
+
rootIsStruct: boolean;
|
|
182
|
+
namedTypes: Map<string, string>;
|
|
183
|
+
typeDecls: NamedType[];
|
|
184
|
+
rootTypeTag: string | undefined;
|
|
185
|
+
paramsType: string;
|
|
186
|
+
sigEntries: SigEntry[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Derive the public names, named-type declarations, root typing, and per-field
|
|
191
|
+
* signature entries for one tool. Mutates `scope` exactly as the emitter needs
|
|
192
|
+
* (the `reg` registrations and the `sigScope` child), so passing the same scope
|
|
193
|
+
* the emitter continues with keeps later local registrations consistent.
|
|
194
|
+
*/
|
|
195
|
+
export function buildEmitModel(
|
|
196
|
+
ctx: CodegenContext,
|
|
197
|
+
scope: Scope = new Scope(PY_RESERVED),
|
|
198
|
+
): PyEmitModel {
|
|
199
|
+
const appId = ctx.app?.id;
|
|
200
|
+
const pkg = ctx.package?.name;
|
|
201
|
+
const publicNames = computePublicNames(appId);
|
|
202
|
+
|
|
203
|
+
const rootBinding = ctx.resolve(ctx.expr);
|
|
204
|
+
const rootType: BoundType = rootBinding?.type ?? { kind: "struct", fields: {} };
|
|
205
|
+
// Only treat the root as struct-shaped when there's a real binding. A
|
|
206
|
+
// synthesized empty-struct fallback (no root binding) means the solver
|
|
207
|
+
// collapsed everything away, so the kwarg wrapper has nothing to wrap.
|
|
208
|
+
const rootIsStruct = rootBinding?.type.kind === "struct";
|
|
209
|
+
|
|
210
|
+
// Pre-reserve module-level public names so any IR-derived names colliding
|
|
211
|
+
// with them get suffix-bumped. `params` is intentionally NOT pre-reserved -
|
|
212
|
+
// `collectNamedTypes` claims it for the root struct just below. Each name
|
|
213
|
+
// is scrubbed through `pyScrubIdent` first since the case helpers happily
|
|
214
|
+
// pass through digit-leading app ids like `3dvolreg.afni`.
|
|
215
|
+
const reg = (name: string) => scope.add(pyScrubIdent(name, PY_RESERVED));
|
|
216
|
+
const names = {
|
|
217
|
+
params: pyScrubIdent(publicNames.params, PY_RESERVED),
|
|
218
|
+
outputs: reg(publicNames.outputs),
|
|
219
|
+
metadata: reg(publicNames.metadata),
|
|
220
|
+
cargs: reg(publicNames.cargs),
|
|
221
|
+
outputsFn: reg(publicNames.outputsFn),
|
|
222
|
+
paramsFn: rootIsStruct ? reg(publicNames.paramsFn) : "",
|
|
223
|
+
execute: rootIsStruct ? reg(publicNames.execute) : "",
|
|
224
|
+
validate: reg(publicNames.validate),
|
|
225
|
+
wrapper: reg(publicNames.wrapper),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Prefix nested type names with the tool's root name so a suite's flat
|
|
229
|
+
// `from .x import *` re-exports don't shadow same-named types across tools.
|
|
230
|
+
const { namedTypes, typeDecls } = collectNamedTypes(
|
|
231
|
+
rootType,
|
|
232
|
+
names.params,
|
|
233
|
+
scope,
|
|
234
|
+
pascalCase,
|
|
235
|
+
appId ? names.params : "",
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
names.params =
|
|
239
|
+
(rootType.kind === "struct" ? namedTypes.get(structKey(rootType)) : undefined) ??
|
|
240
|
+
(rootType.kind === "union" ? namedTypes.get(unionKey(rootType)) : undefined) ??
|
|
241
|
+
names.params;
|
|
242
|
+
|
|
243
|
+
// Tag injected as `@type: Literal[...]` on the root TypedDict (and as a
|
|
244
|
+
// constant key by the params factory). Skipped when appId/pkg aren't known.
|
|
245
|
+
const rootTypeTag = appId && pkg ? `${pkg}/${appId}` : undefined;
|
|
246
|
+
|
|
247
|
+
const paramsType =
|
|
248
|
+
rootType.kind === "struct" || rootType.kind === "union"
|
|
249
|
+
? names.params
|
|
250
|
+
: mapType(rootType, resolveTypeName(namedTypes));
|
|
251
|
+
|
|
252
|
+
// Build the per-field SigEntry list once - the factory and kwarg wrapper
|
|
253
|
+
// both consume it, so the host names registered here must satisfy both
|
|
254
|
+
// function scopes. Pre-reserve `params` (factory + wrapper body) and
|
|
255
|
+
// `runner` (wrapper signature) so a wire key matching either gets
|
|
256
|
+
// suffix-bumped. `rootType` is narrowed by `rootIsStruct` for the `Extract`
|
|
257
|
+
// constraint.
|
|
258
|
+
const sigScope = scope.child(["params", "runner"]);
|
|
259
|
+
const sigEntries =
|
|
260
|
+
rootIsStruct && rootType.kind === "struct"
|
|
261
|
+
? buildSigEntries(
|
|
262
|
+
rootType,
|
|
263
|
+
collectFieldInfo(ctx, rootType),
|
|
264
|
+
(wireKey) => sigScope.add(pyScrubIdent(wireKey, PY_RESERVED)),
|
|
265
|
+
pySigOptions(resolveTypeName(namedTypes)),
|
|
266
|
+
)
|
|
267
|
+
: [];
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
appId,
|
|
271
|
+
pkg,
|
|
272
|
+
names,
|
|
273
|
+
rootType,
|
|
274
|
+
rootIsStruct,
|
|
275
|
+
namedTypes,
|
|
276
|
+
typeDecls,
|
|
277
|
+
rootTypeTag,
|
|
278
|
+
paramsType,
|
|
279
|
+
sigEntries,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function generatePython(ctx: CodegenContext, packageScope?: Scope): string {
|
|
284
|
+
const cb = new CodeBuilder(" ");
|
|
285
|
+
// A package-shared scope keeps top-level names unique across every tool in the
|
|
286
|
+
// suite's `from .x import *` re-exports; without one a per-tool scope suffices.
|
|
287
|
+
const scope = packageScope ?? new Scope(PY_RESERVED);
|
|
288
|
+
|
|
289
|
+
const {
|
|
290
|
+
names,
|
|
291
|
+
rootType,
|
|
292
|
+
rootIsStruct,
|
|
293
|
+
namedTypes,
|
|
294
|
+
typeDecls,
|
|
295
|
+
rootTypeTag,
|
|
296
|
+
paramsType,
|
|
297
|
+
sigEntries,
|
|
298
|
+
} = buildEmitModel(ctx, scope);
|
|
299
|
+
|
|
300
|
+
// Auto-generated header.
|
|
301
|
+
cb.comment("This file was auto generated by Styx.", "# ");
|
|
302
|
+
cb.comment("Do not edit this file directly.", "# ");
|
|
303
|
+
cb.blank();
|
|
304
|
+
|
|
305
|
+
// Every tool emits an Outputs object: at minimum the synthetic `root` output
|
|
306
|
+
// directory (output_file(".")), plus any declared file/mutable outputs and
|
|
307
|
+
// stdout/stderr stream fields. OutputPathType is therefore always imported.
|
|
308
|
+
const emitOutputs = true;
|
|
309
|
+
|
|
310
|
+
emitImports(cb, true);
|
|
311
|
+
cb.blank();
|
|
312
|
+
|
|
313
|
+
emitMetadata(ctx, names.metadata, cb);
|
|
314
|
+
cb.blank();
|
|
315
|
+
|
|
316
|
+
emitTypeDeclarations(typeDecls, namedTypes, ctx, cb, names.params, rootTypeTag);
|
|
317
|
+
|
|
318
|
+
if (emitOutputs) {
|
|
319
|
+
emitOutputsClass(ctx, names.outputs, cb);
|
|
320
|
+
cb.blank();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (emitOutputs && needsStripExtensionsHelper(ctx)) {
|
|
324
|
+
emitStripExtensionsHelper(cb);
|
|
325
|
+
cb.blank();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Params factory (struct-rooted tools only): a kwarg-style builder for the
|
|
329
|
+
// params dict. Useful for callers that want to build a dict to mutate before
|
|
330
|
+
// executing.
|
|
331
|
+
if (rootIsStruct) {
|
|
332
|
+
emitParamsFactory(sigEntries, names.paramsFn, paramsType, rootTypeTag, cb);
|
|
333
|
+
cb.blank();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Validation: walks the root binding and raises StyxValidationError on bad
|
|
337
|
+
// input. Called first thing in the dict-style execute (below).
|
|
338
|
+
emitValidate(
|
|
339
|
+
ctx,
|
|
340
|
+
rootType,
|
|
341
|
+
ctx.expr,
|
|
342
|
+
paramsType,
|
|
343
|
+
names.validate,
|
|
344
|
+
resolveTypeName(namedTypes),
|
|
345
|
+
scope,
|
|
346
|
+
cb,
|
|
347
|
+
);
|
|
348
|
+
cb.blank();
|
|
349
|
+
|
|
350
|
+
emitBuildCargs(ctx, rootType, paramsType, names.cargs, cb);
|
|
351
|
+
cb.blank();
|
|
352
|
+
|
|
353
|
+
if (emitOutputs) {
|
|
354
|
+
emitBuildOutputs(ctx, paramsType, names.outputs, names.outputsFn, cb);
|
|
355
|
+
cb.blank();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Dict-style execute function. For struct roots it's the internal
|
|
359
|
+
// `<tool>_execute`; for other roots it doubles as the user-facing wrapper.
|
|
360
|
+
const executeName = rootIsStruct ? names.execute : names.wrapper;
|
|
361
|
+
emitWrapperFunction(
|
|
362
|
+
ctx,
|
|
363
|
+
paramsType,
|
|
364
|
+
executeName,
|
|
365
|
+
names.metadata,
|
|
366
|
+
names.cargs,
|
|
367
|
+
emitOutputs ? names.outputsFn : undefined,
|
|
368
|
+
emitOutputs ? names.outputs : undefined,
|
|
369
|
+
names.validate,
|
|
370
|
+
streamFieldIds(ctx),
|
|
371
|
+
cb,
|
|
372
|
+
);
|
|
373
|
+
cb.blank();
|
|
374
|
+
|
|
375
|
+
// Kwarg-style wrapper (struct roots only): the v1-parity user-facing entry.
|
|
376
|
+
if (rootIsStruct) {
|
|
377
|
+
emitKwargWrapper(
|
|
378
|
+
ctx,
|
|
379
|
+
sigEntries,
|
|
380
|
+
names.wrapper,
|
|
381
|
+
names.paramsFn,
|
|
382
|
+
names.execute,
|
|
383
|
+
emitOutputs ? names.outputs : undefined,
|
|
384
|
+
cb,
|
|
385
|
+
);
|
|
386
|
+
cb.blank();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// `__all__` keeps suite-level `from .bet import *` from re-exporting the
|
|
390
|
+
// module's stdlib/styxdefs imports.
|
|
391
|
+
const publicSymbols = [
|
|
392
|
+
names.params,
|
|
393
|
+
...(emitOutputs ? [names.outputs] : []),
|
|
394
|
+
names.metadata,
|
|
395
|
+
names.cargs,
|
|
396
|
+
...(emitOutputs ? [names.outputsFn] : []),
|
|
397
|
+
...(rootIsStruct ? [names.paramsFn, names.execute] : []),
|
|
398
|
+
names.validate,
|
|
399
|
+
names.wrapper,
|
|
400
|
+
];
|
|
401
|
+
cb.line("__all__ = [");
|
|
402
|
+
cb.indent(() => {
|
|
403
|
+
for (const sym of publicSymbols) cb.line(`"${sym}",`);
|
|
404
|
+
});
|
|
405
|
+
cb.line("]");
|
|
406
|
+
|
|
407
|
+
return cb.toString();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Module name (file stem) for an app: snake_case of app.id, fallback `output`.
|
|
412
|
+
* Scrubbed so digit-leading app ids (e.g. `3dPFM` -> `v_3d_pfm`) and keyword
|
|
413
|
+
* collisions don't break `from .<mod> import *` in the package __init__.
|
|
414
|
+
*/
|
|
415
|
+
export function appModuleName(meta: AppMeta | undefined): string {
|
|
416
|
+
if (!meta?.id) return "output";
|
|
417
|
+
return pyScrubIdent(snakeCase(meta.id), PY_RESERVED);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The dispatch entrypoint for one app: its root `@type` (`<package>/<app>`) and
|
|
422
|
+
* the dict-style execute function name. Returns undefined when the id or package
|
|
423
|
+
* is unknown (no stable `@type`), so the app is left out of the suite dispatcher.
|
|
424
|
+
*/
|
|
425
|
+
export function appEntrypoint(ctx: CodegenContext): AppEntrypoint | undefined {
|
|
426
|
+
const appId = ctx.app?.id;
|
|
427
|
+
const pkg = ctx.package?.name;
|
|
428
|
+
if (!appId || !pkg) return undefined;
|
|
429
|
+
const publicNames = computePublicNames(appId);
|
|
430
|
+
const rootIsStruct = ctx.resolve(ctx.expr)?.type.kind === "struct";
|
|
431
|
+
const executeFn = pyScrubIdent(
|
|
432
|
+
rootIsStruct ? publicNames.execute : publicNames.wrapper,
|
|
433
|
+
PY_RESERVED,
|
|
434
|
+
);
|
|
435
|
+
return { type: `${pkg}/${appId}`, executeFn };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Generate the suite-level `__init__.py` re-export for a package containing
|
|
440
|
+
* multiple tool modules. Each tool module's public symbols are surfaced via
|
|
441
|
+
* `from .bet import *` (each tool file defines `__all__`). When apps carry a
|
|
442
|
+
* dispatch entrypoint, a suite-level `execute(params, runner)` is appended that
|
|
443
|
+
* routes a config object to the right tool by its root `@type`.
|
|
444
|
+
*/
|
|
445
|
+
export function generatePackageInit(apps: EmittedApp[]): string {
|
|
446
|
+
const cb = new CodeBuilder(" ");
|
|
447
|
+
cb.comment("This file was auto generated by Styx.", "# ");
|
|
448
|
+
cb.comment("Do not edit this file directly.", "# ");
|
|
449
|
+
cb.blank();
|
|
450
|
+
|
|
451
|
+
const dispatch = apps
|
|
452
|
+
.map((a) => a.entrypoint)
|
|
453
|
+
.filter((e): e is AppEntrypoint => e !== undefined)
|
|
454
|
+
.sort((a, b) => a.type.localeCompare(b.type));
|
|
455
|
+
|
|
456
|
+
if (dispatch.length > 0) {
|
|
457
|
+
cb.line("import typing");
|
|
458
|
+
cb.blank();
|
|
459
|
+
cb.line("from styxdefs import Runner");
|
|
460
|
+
cb.blank();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const modules = apps
|
|
464
|
+
.map((a) => appModuleName(a.meta))
|
|
465
|
+
.filter((name): name is string => !!name)
|
|
466
|
+
.sort();
|
|
467
|
+
|
|
468
|
+
for (const mod of modules) {
|
|
469
|
+
cb.line(`from .${mod} import *`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (dispatch.length > 0) {
|
|
473
|
+
cb.blank();
|
|
474
|
+
emitPackageDispatch(cb, dispatch);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return cb.toString();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Emit the suite-level `execute(params, runner)` dispatcher over `@type`. */
|
|
481
|
+
function emitPackageDispatch(cb: CodeBuilder, dispatch: AppEntrypoint[]): void {
|
|
482
|
+
cb.line(
|
|
483
|
+
"def execute(params: dict[str, typing.Any], runner: Runner | None = None) -> typing.Any:",
|
|
484
|
+
);
|
|
485
|
+
cb.indent(() => {
|
|
486
|
+
cb.line('"""Run a tool in this package from a params object, routed by its `@type`."""');
|
|
487
|
+
cb.line("_dispatch: dict[str, typing.Callable[[typing.Any, Runner | None], typing.Any]] = {");
|
|
488
|
+
cb.indent(() => {
|
|
489
|
+
for (const e of dispatch) {
|
|
490
|
+
cb.line(`${JSON.stringify(e.type)}: ${e.executeFn},`);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
cb.line("}");
|
|
494
|
+
cb.line('_fn = _dispatch.get(params["@type"])');
|
|
495
|
+
cb.line("if _fn is None:");
|
|
496
|
+
cb.indent(() => {
|
|
497
|
+
cb.line(`raise ValueError(f"No tool registered for @type {params['@type']!r}")`);
|
|
498
|
+
});
|
|
499
|
+
cb.line("return _fn(params, runner)");
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export class PythonBackend implements Backend {
|
|
504
|
+
readonly name = "python";
|
|
505
|
+
readonly target = "python";
|
|
506
|
+
|
|
507
|
+
emitApp(ctx: CodegenContext, scope?: Scope): EmittedApp {
|
|
508
|
+
const code = generatePython(ctx, scope);
|
|
509
|
+
const fileName = `${appModuleName(ctx.app)}.py`;
|
|
510
|
+
return {
|
|
511
|
+
meta: ctx.app,
|
|
512
|
+
entrypoint: appEntrypoint(ctx),
|
|
513
|
+
files: new Map([[fileName, code]]),
|
|
514
|
+
errors: [],
|
|
515
|
+
warnings: [],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
newPackageScope(): Scope {
|
|
520
|
+
return new Scope(PY_RESERVED);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
emitPackage(pkg: PackageMeta, apps: EmittedApp[]): EmittedPackage {
|
|
524
|
+
return {
|
|
525
|
+
meta: pkg,
|
|
526
|
+
files: new Map([
|
|
527
|
+
["__init__.py", generatePackageInit(apps)],
|
|
528
|
+
// PEP 561 marker so type-checkers treat the generated suite as typed.
|
|
529
|
+
["py.typed", ""],
|
|
530
|
+
]),
|
|
531
|
+
errors: [],
|
|
532
|
+
warnings: [],
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
emitProject(proj: ProjectMeta, packages: EmittedPackage[]): EmitResult {
|
|
537
|
+
const files = new Map<string, string>();
|
|
538
|
+
const distNames: string[] = [];
|
|
539
|
+
const pkgDirs: string[] = [];
|
|
540
|
+
|
|
541
|
+
for (const p of packages) {
|
|
542
|
+
const pkg = p.meta ?? {};
|
|
543
|
+
// Mirror the CLI's `pkgDir` fallback so a nameless package's source dir
|
|
544
|
+
// still gets a matching pyproject/README instead of being orphaned.
|
|
545
|
+
const dir = pkg.name ?? "package";
|
|
546
|
+
pkgDirs.push(dir);
|
|
547
|
+
distNames.push(pyDistName(proj, pkg));
|
|
548
|
+
files.set(`${dir}/pyproject.toml`, generateSubPyproject(proj, pkg));
|
|
549
|
+
files.set(`${dir}/README.md`, generateSubReadme(proj, pkg));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
files.set("pyproject.toml", generateRootPyproject(proj, distNames));
|
|
553
|
+
files.set("README.md", generateRootReadme(proj, distNames));
|
|
554
|
+
files.set("requirements.txt", generateRequirementsTxt(pkgDirs));
|
|
555
|
+
|
|
556
|
+
return { files, errors: [], warnings: [] };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { BoundType } from "../../bindings/index.js";
|
|
2
|
+
import type { CodegenContext } from "../../manifest/index.js";
|
|
3
|
+
import type { SigEntry } from "../sig-entries.js";
|
|
4
|
+
import type { SnippetDialect, SnippetOptions } from "../snippet-core.js";
|
|
5
|
+
import { renderValue } from "../snippet-core.js";
|
|
6
|
+
import { buildEmitModel } from "./python.js";
|
|
7
|
+
import { pyStr } from "./typemap.js";
|
|
8
|
+
|
|
9
|
+
/** Snippet rendering hooks for Python (dict literals, `True`/`None`). */
|
|
10
|
+
const pyDialect: SnippetDialect = {
|
|
11
|
+
indentUnit: " ",
|
|
12
|
+
string: pyStr,
|
|
13
|
+
boolean: (b) => (b ? "True" : "False"),
|
|
14
|
+
number: (n) => (Number.isFinite(n) ? String(n) : "float('nan')"),
|
|
15
|
+
null: "None",
|
|
16
|
+
objKey: (k) => pyStr(k),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render a Python call snippet for one tool from a config object (the params
|
|
21
|
+
* dict the form produces, keyed by Boutiques wire names).
|
|
22
|
+
*
|
|
23
|
+
* Struct-rooted tools use the ergonomic kwarg wrapper -
|
|
24
|
+
* `fsl.bet(infile=..., fractional_intensity=0.5)` - whose keyword names are the
|
|
25
|
+
* *scrubbed host* identifiers (`float` -> `float_`), not the wire keys; the
|
|
26
|
+
* per-field mapping comes from the same `buildSigEntries` the generated wrapper
|
|
27
|
+
* is built from, so the snippet matches the real signature. Nested structs /
|
|
28
|
+
* union variants / lists-of-structs have no constructor in the generated code,
|
|
29
|
+
* so they render as plain dict literals keyed by wire names.
|
|
30
|
+
*
|
|
31
|
+
* Union- (or otherwise non-struct-) rooted tools have no kwarg wrapper; the
|
|
32
|
+
* single dict-style `<tool>` entry is called with one object-literal argument.
|
|
33
|
+
*
|
|
34
|
+
* The snippet matches the *standalone* (single-descriptor) emission of the same
|
|
35
|
+
* context - which is how the hub compiles - not a catalog emission where a
|
|
36
|
+
* shared package scope could suffix-bump a name.
|
|
37
|
+
*
|
|
38
|
+
* @param ctx - The compiled context (compile -> pipeline -> solve ->
|
|
39
|
+
* resolveOutputs -> createContext, as in the CLI's `readAndCompile`).
|
|
40
|
+
* @param config - The params object, keyed by Boutiques *wire* names (not host
|
|
41
|
+
* identifiers). Every union-typed value - including the root of a union-rooted
|
|
42
|
+
* tool - must carry its `@type` discriminator so the variant can be matched;
|
|
43
|
+
* the root struct's `@type` is supplied by the renderer, so omit it there.
|
|
44
|
+
* @param opts - Import and package-root options.
|
|
45
|
+
*/
|
|
46
|
+
export function renderPythonCall(
|
|
47
|
+
ctx: CodegenContext,
|
|
48
|
+
config: Record<string, unknown>,
|
|
49
|
+
opts: SnippetOptions = {},
|
|
50
|
+
): string {
|
|
51
|
+
const model = buildEmitModel(ctx);
|
|
52
|
+
const pkg = ctx.package?.name;
|
|
53
|
+
const callee = pkg ? `${pkg}.${model.names.wrapper}` : model.names.wrapper;
|
|
54
|
+
|
|
55
|
+
const call =
|
|
56
|
+
model.rootIsStruct && model.rootType.kind === "struct"
|
|
57
|
+
? renderKwargCall(callee, model.sigEntries, model.rootType, config)
|
|
58
|
+
: `${callee}(${renderValue(config, model.rootType, "", pyDialect)})`;
|
|
59
|
+
|
|
60
|
+
if (opts.includeImport === false || !pkg) return call;
|
|
61
|
+
const root = opts.packageRoot ?? ctx.project?.name;
|
|
62
|
+
const importLine = root ? `from ${root} import ${pkg}` : `import ${pkg}`;
|
|
63
|
+
return `${importLine}\n\n${call}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Render `callee(name=value, ...)` for a struct root using scrubbed kwarg names. */
|
|
67
|
+
function renderKwargCall(
|
|
68
|
+
callee: string,
|
|
69
|
+
sigEntries: readonly SigEntry[],
|
|
70
|
+
rootType: Extract<BoundType, { kind: "struct" }>,
|
|
71
|
+
config: Record<string, unknown>,
|
|
72
|
+
): string {
|
|
73
|
+
const nameFor = new Map(sigEntries.map((e) => [e.wireKey, e.name]));
|
|
74
|
+
const indent = pyDialect.indentUnit;
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
for (const [wireKey, fieldType] of Object.entries(rootType.fields)) {
|
|
77
|
+
if (fieldType.kind === "literal") continue; // @type / consts injected by the wrapper
|
|
78
|
+
if (!(wireKey in config)) continue;
|
|
79
|
+
const name = nameFor.get(wireKey) ?? wireKey;
|
|
80
|
+
lines.push(`${indent}${name}=${renderValue(config[wireKey], fieldType, indent, pyDialect)},`);
|
|
81
|
+
}
|
|
82
|
+
if (lines.length === 0) return `${callee}()`;
|
|
83
|
+
return `${callee}(\n${lines.join("\n")}\n)`;
|
|
84
|
+
}
|