@styx-api/core 0.3.0 → 0.5.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 +1027 -273
- package/dist/index.d.cts +226 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +226 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1020 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/backend/index.ts +13 -1
- package/src/backend/nipype/emit.ts +270 -0
- package/src/backend/nipype/index.ts +3 -0
- package/src/backend/nipype/nipype.ts +70 -0
- package/src/backend/pydra/emit.ts +318 -0
- package/src/backend/pydra/index.ts +3 -0
- package/src/backend/pydra/pydra.ts +67 -0
- package/src/backend/python/index.ts +6 -1
- package/src/backend/python/outputs-emit.ts +1 -1
- package/src/backend/python/packaging.ts +33 -5
- package/src/backend/python/python.ts +28 -3
- package/src/backend/schema/jsonschema.ts +34 -4
- package/src/backend/styxdefs-compat.ts +6 -4
- package/src/backend/typed-spec.ts +258 -0
- package/src/frontend/mrtrix/parser.ts +51 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@styx-api/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Styx compiler core: parses CLI tool descriptors (e.g. Boutiques), optimizes an IR, solves typed parameter bindings, and generates type-safe wrappers (TypeScript, Python, JSON Schema). Part of the Styx/NiWrap ecosystem.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"styx",
|
package/src/backend/index.ts
CHANGED
|
@@ -16,6 +16,10 @@ export type { NamedType } from "./collect-named-types.js";
|
|
|
16
16
|
export { collectNamedTypes, resolveTypeName } from "./collect-named-types.js";
|
|
17
17
|
export { findDoc } from "./find-doc.js";
|
|
18
18
|
export { findStructNode } from "./find-struct-node.js";
|
|
19
|
+
export type { NipypeNames } from "./nipype/index.js";
|
|
20
|
+
export { generateNipype, NipypeBackend, nipypeNames } from "./nipype/index.js";
|
|
21
|
+
export type { PydraNames } from "./pydra/index.js";
|
|
22
|
+
export { generatePydra, PydraBackend, pydraNames } from "./pydra/index.js";
|
|
19
23
|
export { resolveFieldBinding } from "./resolve-field-binding.js";
|
|
20
24
|
export type { OutputEmitPlan } from "./resolve-output-tokens.js";
|
|
21
25
|
export {
|
|
@@ -26,7 +30,7 @@ export {
|
|
|
26
30
|
planOutput,
|
|
27
31
|
planScope,
|
|
28
32
|
} from "./resolve-output-tokens.js";
|
|
29
|
-
export { generatePython, PythonBackend, renderPythonCall } from "./python/index.js";
|
|
33
|
+
export { buildEmitModel, generatePython, PythonBackend, renderPythonCall } from "./python/index.js";
|
|
30
34
|
export { Scope } from "./scope.js";
|
|
31
35
|
export type { JsonSchema } from "./schema/index.js";
|
|
32
36
|
export { generateOutputsSchema, generateSchema, JsonSchemaBackend } from "./schema/index.js";
|
|
@@ -36,6 +40,14 @@ export { renderStructLiteral, renderValue } from "./snippet-core.js";
|
|
|
36
40
|
export type { SnippetDialect, SnippetOptions } from "./snippet-core.js";
|
|
37
41
|
export { camelCase, pascalCase, screamingSnakeCase, snakeCase } from "./string-case.js";
|
|
38
42
|
export { PYTHON_RUNNER_DEPS, STYXDEFS_COMPAT } from "./styxdefs-compat.js";
|
|
43
|
+
export type {
|
|
44
|
+
DelegationTarget,
|
|
45
|
+
TypedParam,
|
|
46
|
+
TypedParamItem,
|
|
47
|
+
TypedParamKind,
|
|
48
|
+
TypedSpec,
|
|
49
|
+
} from "./typed-spec.js";
|
|
50
|
+
export { buildTypedSpec } from "./typed-spec.js";
|
|
39
51
|
export { structKey, typeKey, unionKey } from "./type-keys.js";
|
|
40
52
|
export {
|
|
41
53
|
appEntrypoint,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { CodegenContext } from "../../manifest/index.js";
|
|
2
|
+
import { CodeBuilder } from "../code-builder.js";
|
|
3
|
+
import type { OutputField } from "../collect-output-fields.js";
|
|
4
|
+
import { emitDocstring } from "../python/emit.js";
|
|
5
|
+
import { pyStr, renderPyLiteral } from "../python/typemap.js";
|
|
6
|
+
import type { TypedParam, TypedParamItem, TypedSpec } from "../typed-spec.js";
|
|
7
|
+
|
|
8
|
+
/** Generated symbol names for one tool's nipype interface module. */
|
|
9
|
+
export interface NipypeNames {
|
|
10
|
+
/** Styx Python module file stem to import the wrapper from (e.g. `_bet`). */
|
|
11
|
+
styxStem: string;
|
|
12
|
+
/** Interface module file stem (e.g. `bet`). */
|
|
13
|
+
ifaceStem: string;
|
|
14
|
+
/** Interface class name (e.g. `Bet`). */
|
|
15
|
+
cls: string;
|
|
16
|
+
/** Input spec class name (e.g. `BetInputSpec`). */
|
|
17
|
+
inputSpec: string;
|
|
18
|
+
/** Output spec class name (e.g. `BetOutputSpec`). */
|
|
19
|
+
outputSpec: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function call(ctor: string, args: string[]): string {
|
|
23
|
+
return `${ctor}(${args.join(", ")})`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render a numeric literal, forcing a float form (e.g. `0.0`) for a float field.
|
|
28
|
+
* `traits.Range` infers its numeric type from the bound literals, so integer
|
|
29
|
+
* bounds on a float field (e.g. `low=0, high=1`) would build an *integer* range
|
|
30
|
+
* that rejects `0.5`. Emitting `0.0`/`1.0` keeps it a float range.
|
|
31
|
+
*/
|
|
32
|
+
function renderNum(value: string | number | boolean, asFloat: boolean): string {
|
|
33
|
+
if (asFloat && typeof value === "number" && Number.isInteger(value)) return `${value}.0`;
|
|
34
|
+
return renderPyLiteral(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Human-readable `desc=` text: doc + degrade/media-type notes. */
|
|
38
|
+
function descText(p: TypedParam): string | undefined {
|
|
39
|
+
const parts: string[] = [];
|
|
40
|
+
if (p.doc) parts.push(p.doc);
|
|
41
|
+
if (p.kind === "struct" || p.kind === "union") parts.push("(nested configuration; pass a dict)");
|
|
42
|
+
if (p.mediaTypes && p.mediaTypes.length > 0) {
|
|
43
|
+
parts.push(`(media types: ${p.mediaTypes.join(", ")})`);
|
|
44
|
+
}
|
|
45
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Map a list element descriptor to a (bare) nipype inner trait. */
|
|
49
|
+
function renderItemTrait(item: TypedParamItem | undefined): string {
|
|
50
|
+
if (!item) return "traits.Any()";
|
|
51
|
+
switch (item.kind) {
|
|
52
|
+
case "path":
|
|
53
|
+
return "File(exists=True)";
|
|
54
|
+
case "int":
|
|
55
|
+
case "count":
|
|
56
|
+
return "traits.Int()";
|
|
57
|
+
case "float":
|
|
58
|
+
return "traits.Float()";
|
|
59
|
+
case "str":
|
|
60
|
+
return "traits.Str()";
|
|
61
|
+
case "bool":
|
|
62
|
+
return "traits.Bool()";
|
|
63
|
+
case "enum":
|
|
64
|
+
return call(
|
|
65
|
+
"traits.Enum",
|
|
66
|
+
(item.choices ?? []).map((c) => renderPyLiteral(c)),
|
|
67
|
+
);
|
|
68
|
+
default:
|
|
69
|
+
return "traits.Any()";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Map a parameter to its nipype input trait expression, carrying rich constraints. */
|
|
74
|
+
export function renderInputTrait(p: TypedParam): string {
|
|
75
|
+
const tail: string[] = [];
|
|
76
|
+
if (p.mandatory) tail.push("mandatory=True");
|
|
77
|
+
const desc = descText(p);
|
|
78
|
+
if (desc) tail.push(`desc=${pyStr(desc)}`);
|
|
79
|
+
const hasDef = p.hasDefault && p.default !== undefined;
|
|
80
|
+
const def = p.default;
|
|
81
|
+
|
|
82
|
+
switch (p.kind) {
|
|
83
|
+
case "path":
|
|
84
|
+
return call("File", ["exists=True", ...tail]);
|
|
85
|
+
case "bool":
|
|
86
|
+
return call("traits.Bool", [
|
|
87
|
+
...(hasDef ? [renderPyLiteral(def!), "usedefault=True"] : []),
|
|
88
|
+
...tail,
|
|
89
|
+
]);
|
|
90
|
+
case "count":
|
|
91
|
+
return call("traits.Int", [
|
|
92
|
+
...(hasDef ? [renderPyLiteral(def!), "usedefault=True"] : []),
|
|
93
|
+
...tail,
|
|
94
|
+
]);
|
|
95
|
+
case "int":
|
|
96
|
+
case "float": {
|
|
97
|
+
const asFloat = p.kind === "float";
|
|
98
|
+
if (p.range) {
|
|
99
|
+
const a: string[] = [];
|
|
100
|
+
if (hasDef) a.push(`value=${renderNum(def!, asFloat)}`);
|
|
101
|
+
if (p.range.min !== undefined) a.push(`low=${renderNum(p.range.min, asFloat)}`);
|
|
102
|
+
if (p.range.max !== undefined) a.push(`high=${renderNum(p.range.max, asFloat)}`);
|
|
103
|
+
if (hasDef) a.push("usedefault=True");
|
|
104
|
+
return call("traits.Range", [...a, ...tail]);
|
|
105
|
+
}
|
|
106
|
+
const ctor = p.kind === "int" ? "traits.Int" : "traits.Float";
|
|
107
|
+
return call(ctor, [
|
|
108
|
+
...(hasDef ? [renderNum(def!, asFloat), "usedefault=True"] : []),
|
|
109
|
+
...tail,
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
case "str":
|
|
113
|
+
return call("traits.Str", [
|
|
114
|
+
...(hasDef ? [renderPyLiteral(def!), "usedefault=True"] : []),
|
|
115
|
+
...tail,
|
|
116
|
+
]);
|
|
117
|
+
case "enum": {
|
|
118
|
+
const choices = p.choices ?? [];
|
|
119
|
+
// nipype's first Enum arg is the default; put the styx default first.
|
|
120
|
+
const ordered = hasDef ? [def!, ...choices.filter((c) => c !== def)] : choices;
|
|
121
|
+
return call("traits.Enum", [
|
|
122
|
+
...ordered.map((c) => renderPyLiteral(c)),
|
|
123
|
+
...(hasDef ? ["usedefault=True"] : []),
|
|
124
|
+
...tail,
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
case "list": {
|
|
128
|
+
const inner = renderItemTrait(p.itemType);
|
|
129
|
+
const bounds: string[] = [];
|
|
130
|
+
if (p.listBounds?.min !== undefined) bounds.push(`minlen=${p.listBounds.min}`);
|
|
131
|
+
if (p.listBounds?.max !== undefined) bounds.push(`maxlen=${p.listBounds.max}`);
|
|
132
|
+
return call("traits.List", [inner, ...bounds, ...tail]);
|
|
133
|
+
}
|
|
134
|
+
case "struct":
|
|
135
|
+
case "union":
|
|
136
|
+
return call("traits.Any", tail);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Map an output field to its nipype output trait expression. `isRoot` is the
|
|
142
|
+
* synthetic output directory, typed as a `Directory` rather than a `File`.
|
|
143
|
+
*/
|
|
144
|
+
function renderOutputTrait(f: OutputField, isRoot: boolean): string {
|
|
145
|
+
const tail = f.doc ? [`desc=${pyStr(f.doc)}`] : [];
|
|
146
|
+
if (f.shape.kind === "list") return call("traits.List", ["File()", ...tail]);
|
|
147
|
+
if (isRoot) return call("Directory", tail);
|
|
148
|
+
return call("File", tail);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Emit the nipype interface module for one tool: a typed InputSpec/OutputSpec and
|
|
153
|
+
* a BaseInterface that delegates execution to the co-emitted styx Python wrapper.
|
|
154
|
+
*/
|
|
155
|
+
export function emitNipypeInterface(
|
|
156
|
+
ctx: CodegenContext,
|
|
157
|
+
spec: TypedSpec,
|
|
158
|
+
names: NipypeNames,
|
|
159
|
+
): string {
|
|
160
|
+
const cb = new CodeBuilder(" ");
|
|
161
|
+
cb.comment("This file was auto generated by Styx.", "# ");
|
|
162
|
+
cb.comment("Do not edit this file directly.", "# ");
|
|
163
|
+
cb.blank();
|
|
164
|
+
|
|
165
|
+
cb.line("from nipype.interfaces.base import (");
|
|
166
|
+
cb.indent(() => {
|
|
167
|
+
cb.line("BaseInterface,");
|
|
168
|
+
cb.line("BaseInterfaceInputSpec,");
|
|
169
|
+
cb.line("Directory,");
|
|
170
|
+
cb.line("File,");
|
|
171
|
+
cb.line("TraitedSpec,");
|
|
172
|
+
cb.line("isdefined,");
|
|
173
|
+
cb.line("traits,");
|
|
174
|
+
});
|
|
175
|
+
cb.line(")");
|
|
176
|
+
cb.blank();
|
|
177
|
+
cb.line(
|
|
178
|
+
`from .${names.styxStem} import ${spec.delegation.wrapperFn}, ${spec.delegation.outputsClass}`,
|
|
179
|
+
);
|
|
180
|
+
cb.blank();
|
|
181
|
+
cb.blank();
|
|
182
|
+
|
|
183
|
+
// InputSpec
|
|
184
|
+
cb.line(`class ${names.inputSpec}(BaseInterfaceInputSpec):`);
|
|
185
|
+
cb.indent(() => {
|
|
186
|
+
if (!spec.rootIsStruct || spec.params.length === 0) {
|
|
187
|
+
cb.line("pass");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const p of spec.params) {
|
|
191
|
+
cb.line(`${p.hostName} = ${renderInputTrait(p)}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
cb.blank();
|
|
195
|
+
cb.blank();
|
|
196
|
+
|
|
197
|
+
// OutputSpec
|
|
198
|
+
cb.line(`class ${names.outputSpec}(TraitedSpec):`);
|
|
199
|
+
cb.indent(() => {
|
|
200
|
+
if (spec.outputs.length === 0 && spec.streams.length === 0) {
|
|
201
|
+
cb.line("pass");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
spec.outputs.forEach((f, i) => cb.line(`${f.id} = ${renderOutputTrait(f, i === 0)}`));
|
|
205
|
+
for (const s of spec.streams) {
|
|
206
|
+
const tail = s.doc ? `, desc=${pyStr(s.doc)}` : "";
|
|
207
|
+
cb.line(`${s.id} = traits.List(traits.Str()${tail})`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
cb.blank();
|
|
211
|
+
cb.blank();
|
|
212
|
+
|
|
213
|
+
// Interface
|
|
214
|
+
cb.line(`class ${names.cls}(BaseInterface):`);
|
|
215
|
+
cb.indent(() => {
|
|
216
|
+
const docText = [ctx.app?.doc?.title, ctx.app?.doc?.description].filter(Boolean).join("\n\n");
|
|
217
|
+
emitDocstring(cb, docText || undefined);
|
|
218
|
+
cb.line(`input_spec = ${names.inputSpec}`);
|
|
219
|
+
cb.line(`output_spec = ${names.outputSpec}`);
|
|
220
|
+
cb.blank();
|
|
221
|
+
|
|
222
|
+
cb.line("def _run_interface(self, runtime):");
|
|
223
|
+
cb.indent(() => {
|
|
224
|
+
if (!spec.rootIsStruct) {
|
|
225
|
+
cb.line("raise NotImplementedError(");
|
|
226
|
+
cb.indent(() =>
|
|
227
|
+
cb.line(pyStr("styx nipype backend: tools with a non-struct root are not supported.")),
|
|
228
|
+
);
|
|
229
|
+
cb.line(")");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
cb.line("kwargs = {}");
|
|
233
|
+
for (const p of spec.params) {
|
|
234
|
+
if (p.mandatory) {
|
|
235
|
+
cb.line(`kwargs[${pyStr(p.hostName)}] = self.inputs.${p.hostName}`);
|
|
236
|
+
} else {
|
|
237
|
+
cb.line(`if isdefined(self.inputs.${p.hostName}):`);
|
|
238
|
+
cb.indent(() => cb.line(`kwargs[${pyStr(p.hostName)}] = self.inputs.${p.hostName}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
cb.line(
|
|
242
|
+
`self._result: ${spec.delegation.outputsClass} = ${spec.delegation.wrapperFn}(**kwargs)`,
|
|
243
|
+
);
|
|
244
|
+
cb.line("return runtime");
|
|
245
|
+
});
|
|
246
|
+
cb.blank();
|
|
247
|
+
|
|
248
|
+
cb.line("def _list_outputs(self):");
|
|
249
|
+
cb.indent(() => {
|
|
250
|
+
if (!spec.rootIsStruct) {
|
|
251
|
+
cb.line("return self._outputs().get()");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
cb.line("result = self._result");
|
|
255
|
+
cb.line("outputs = self._outputs().get()");
|
|
256
|
+
for (const f of spec.outputs) {
|
|
257
|
+
if (f.shape.kind === "single" && f.shape.optional) {
|
|
258
|
+
cb.line(`if result.${f.id} is not None:`);
|
|
259
|
+
cb.indent(() => cb.line(`outputs[${pyStr(f.id)}] = result.${f.id}`));
|
|
260
|
+
} else {
|
|
261
|
+
cb.line(`outputs[${pyStr(f.id)}] = result.${f.id}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const s of spec.streams) cb.line(`outputs[${pyStr(s.id)}] = result.${s.id}`);
|
|
265
|
+
cb.line("return outputs");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return cb.toString();
|
|
270
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CodegenContext } from "../../manifest/index.js";
|
|
2
|
+
import type { Backend, EmittedApp, EmitWarning } from "../backend.js";
|
|
3
|
+
import type { Scope } from "../scope.js";
|
|
4
|
+
import { appModuleName, generatePython } from "../python/index.js";
|
|
5
|
+
import { pascalCase } from "../string-case.js";
|
|
6
|
+
import { buildTypedSpec } from "../typed-spec.js";
|
|
7
|
+
import { emitNipypeInterface, type NipypeNames } from "./emit.js";
|
|
8
|
+
|
|
9
|
+
/** Derive the per-tool nipype module/class names. */
|
|
10
|
+
export function nipypeNames(ctx: CodegenContext): NipypeNames {
|
|
11
|
+
const mod = appModuleName(ctx.app);
|
|
12
|
+
const rawId = ctx.app?.id ?? "tool";
|
|
13
|
+
// Mirror the Python backend's digit-leading-id prescrub so the derived class
|
|
14
|
+
// name is a valid Python identifier (e.g. `3dPFM` -> `V3DPfm`).
|
|
15
|
+
const safeId = /^[0-9]/.test(rawId) ? "v_" + rawId : rawId;
|
|
16
|
+
const cls = pascalCase(safeId);
|
|
17
|
+
return {
|
|
18
|
+
styxStem: `_${mod}`,
|
|
19
|
+
ifaceStem: mod,
|
|
20
|
+
cls,
|
|
21
|
+
inputSpec: `${cls}InputSpec`,
|
|
22
|
+
outputSpec: `${cls}OutputSpec`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Generate the nipype interface module source for one tool. */
|
|
27
|
+
export function generateNipype(ctx: CodegenContext): string {
|
|
28
|
+
return emitNipypeInterface(ctx, buildTypedSpec(ctx), nipypeNames(ctx));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Emits nipype `Interface` definitions whose typed InputSpec/OutputSpec carry
|
|
33
|
+
* rich constraints (numeric ranges, list bounds, enum choices, file types) and
|
|
34
|
+
* which delegate execution to the styx Python wrapper (Option B): no command-line
|
|
35
|
+
* arg-building or output-path resolution is re-implemented here.
|
|
36
|
+
*
|
|
37
|
+
* Per tool, two co-located files are emitted so the output is a self-contained,
|
|
38
|
+
* importable Python package: `_<tool>.py` (the styx Python module) and
|
|
39
|
+
* `<tool>.py` (the interface, importing the wrapper via a relative import).
|
|
40
|
+
*/
|
|
41
|
+
export class NipypeBackend implements Backend {
|
|
42
|
+
readonly name = "nipype";
|
|
43
|
+
readonly target = "nipype";
|
|
44
|
+
|
|
45
|
+
// `scope` is intentionally unused: each tool is emitted as its own module
|
|
46
|
+
// (no flat re-export barrel), so per-tool fresh scoping keeps the styx module's
|
|
47
|
+
// kwarg names and the typed spec's host names identical by construction.
|
|
48
|
+
// Suite-level shared scoping is a follow-up alongside emitPackage/emitProject.
|
|
49
|
+
emitApp(ctx: CodegenContext, _scope?: Scope): EmittedApp {
|
|
50
|
+
const names = nipypeNames(ctx);
|
|
51
|
+
const spec = buildTypedSpec(ctx);
|
|
52
|
+
const warnings: EmitWarning[] = [];
|
|
53
|
+
if (!spec.rootIsStruct) {
|
|
54
|
+
warnings.push({
|
|
55
|
+
message: `nipype: '${ctx.app?.id ?? "?"}' has a non-struct root; emitted interface has no typed inputs and raises on run.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const styxCode = generatePython(ctx);
|
|
59
|
+
const ifaceCode = emitNipypeInterface(ctx, spec, names);
|
|
60
|
+
return {
|
|
61
|
+
meta: ctx.app,
|
|
62
|
+
files: new Map([
|
|
63
|
+
[`${names.styxStem}.py`, styxCode],
|
|
64
|
+
[`${names.ifaceStem}.py`, ifaceCode],
|
|
65
|
+
]),
|
|
66
|
+
errors: [],
|
|
67
|
+
warnings,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|