@dbx-tools/projen 0.9.46 → 0.9.50
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/README.md +44 -5
- package/index.ts +2 -2
- package/package.json +5 -5
- package/src/project-py.ts +39 -3
- package/src/python-node-bindings.ts +32 -2
- package/src/python-node-bundle.ts +5 -0
- package/tasks/compile-workspace.ts +141 -2
- package/tasks/{lib/local-publish.ts → local-publish.ts} +8 -3
- package/tasks/publish-npm.ts +464 -2
- package/tasks/publish-python.ts +223 -2
- package/tasks/publish.ts +262 -2
- package/tasks/python-node-bindings.ts +1322 -2
- package/tasks/{lib/python-registry.ts → python-registry.ts} +4 -2
- package/tasks/python-sync.ts +248 -0
- package/tasks/release-version.ts +64 -2
- package/tasks/release.ts +272 -2
- package/tasks/lib/compile-workspace.ts +0 -141
- package/tasks/lib/publish-npm.ts +0 -466
- package/tasks/lib/publish-python.ts +0 -213
- package/tasks/lib/publish.ts +0 -264
- package/tasks/lib/python-node-bindings.ts +0 -1343
- package/tasks/lib/release-version.ts +0 -66
- package/tasks/lib/release.ts +0 -278
- /package/tasks/{lib/python-release.ts → python-release.ts} +0 -0
|
@@ -1,1343 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun
|
|
2
|
-
/**
|
|
3
|
-
* Generate a shared PythonMonkey runtime and typed Python wrappers from each
|
|
4
|
-
* package's serialized node-binding configuration.
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
rmSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
14
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
15
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
-
import { parseArgs } from "node:util";
|
|
17
|
-
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
18
|
-
import type { BunPlugin } from "bun";
|
|
19
|
-
import stdLibBrowser from "node-stdlib-browser";
|
|
20
|
-
import ts from "typescript";
|
|
21
|
-
import { header, makeReadonly, makeWritable } from "../../src/generated.ts";
|
|
22
|
-
import { publicFunctionExports, publicNamespaceExports } from "../../src/module-exports.ts";
|
|
23
|
-
import {
|
|
24
|
-
PYTHON_NODE_SHIM_ROOT,
|
|
25
|
-
resolvePythonNodeBindings,
|
|
26
|
-
type ResolvedPythonNodeBindings,
|
|
27
|
-
type ResolvedPythonNodeFunctionOverride,
|
|
28
|
-
} from "../../src/python-node-bindings.ts";
|
|
29
|
-
|
|
30
|
-
const logger = log.logger("projen:python-node-bindings");
|
|
31
|
-
|
|
32
|
-
type FunctionOverride = ResolvedPythonNodeFunctionOverride;
|
|
33
|
-
|
|
34
|
-
function bindingIndex(value: string, count: number, project: string): number {
|
|
35
|
-
const index = Number.parseInt(value, 10);
|
|
36
|
-
if (!Number.isSafeInteger(index) || String(index) !== value || index < 0 || index >= count) {
|
|
37
|
-
throw new Error(`Node binding index ${value} is not configured for ${project}`);
|
|
38
|
-
}
|
|
39
|
-
return index;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const PYTHON_KEYWORDS = new Set([
|
|
43
|
-
"and",
|
|
44
|
-
"as",
|
|
45
|
-
"assert",
|
|
46
|
-
"async",
|
|
47
|
-
"await",
|
|
48
|
-
"break",
|
|
49
|
-
"class",
|
|
50
|
-
"continue",
|
|
51
|
-
"def",
|
|
52
|
-
"del",
|
|
53
|
-
"elif",
|
|
54
|
-
"else",
|
|
55
|
-
"except",
|
|
56
|
-
"False",
|
|
57
|
-
"finally",
|
|
58
|
-
"for",
|
|
59
|
-
"from",
|
|
60
|
-
"global",
|
|
61
|
-
"if",
|
|
62
|
-
"import",
|
|
63
|
-
"in",
|
|
64
|
-
"is",
|
|
65
|
-
"lambda",
|
|
66
|
-
"None",
|
|
67
|
-
"nonlocal",
|
|
68
|
-
"not",
|
|
69
|
-
"or",
|
|
70
|
-
"pass",
|
|
71
|
-
"raise",
|
|
72
|
-
"return",
|
|
73
|
-
"True",
|
|
74
|
-
"try",
|
|
75
|
-
"while",
|
|
76
|
-
"with",
|
|
77
|
-
"yield",
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
|
-
export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
|
|
81
|
-
const { values } = parseArgs({
|
|
82
|
-
args,
|
|
83
|
-
options: {
|
|
84
|
-
binding: { type: "string" },
|
|
85
|
-
check: { type: "boolean" },
|
|
86
|
-
module: { type: "string" },
|
|
87
|
-
project: { type: "string" },
|
|
88
|
-
root: { type: "string" },
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
if (!values.project) throw new Error("Expected --project <python-project-directory>");
|
|
92
|
-
|
|
93
|
-
const root = values.root
|
|
94
|
-
? resolve(values.root)
|
|
95
|
-
: resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
96
|
-
const configs = resolvePythonNodeBindings(root, values.project).map(discoverBindingModules);
|
|
97
|
-
if (configs.length > 1 && configs.some(({ modules }) => modules.length === 0)) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
"Every Node binding must export a namespace with bindable functions or configure modules",
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
const selectedBinding =
|
|
103
|
-
values.binding === undefined
|
|
104
|
-
? undefined
|
|
105
|
-
: bindingIndex(values.binding, configs.length, values.project);
|
|
106
|
-
const config = selectedBinding === undefined ? configs[0] : configs[selectedBinding];
|
|
107
|
-
if (!config) throw new Error("Expected at least one Node binding configuration");
|
|
108
|
-
if (selectedBinding === undefined && (configs.length > 1 || config.modules.length > 0)) {
|
|
109
|
-
prepareBindingDirectory(configs);
|
|
110
|
-
writeRuntimeLoader(configs);
|
|
111
|
-
for (const [binding, candidate] of configs.entries()) {
|
|
112
|
-
for (const module of candidate.modules) {
|
|
113
|
-
await main([
|
|
114
|
-
"--project",
|
|
115
|
-
values.project,
|
|
116
|
-
...(values.root ? ["--root", values.root] : []),
|
|
117
|
-
...(values.check ? ["--check"] : []),
|
|
118
|
-
"--binding",
|
|
119
|
-
String(binding),
|
|
120
|
-
"--module",
|
|
121
|
-
module,
|
|
122
|
-
]);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (config.modules.length > 0 && !values.module) {
|
|
128
|
-
throw new Error(`Expected --module for Node binding ${selectedBinding ?? 0}`);
|
|
129
|
-
}
|
|
130
|
-
const {
|
|
131
|
-
bindingDirectory,
|
|
132
|
-
projectDirectory,
|
|
133
|
-
pyproject,
|
|
134
|
-
runtimeOutput,
|
|
135
|
-
} = config;
|
|
136
|
-
const selectedModule = values.module;
|
|
137
|
-
if (selectedModule && !config.modules.includes(selectedModule)) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Node binding module ${selectedModule} is not configured: ${config.modules.join(", ")}`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const bindingsOutput = join(
|
|
143
|
-
bindingDirectory,
|
|
144
|
-
`${selectedModule ? pythonFunctionName(selectedModule) : "index"}.py`,
|
|
145
|
-
);
|
|
146
|
-
const packageEntrypoint = Bun.resolveSync(config.entrypoint, projectDirectory);
|
|
147
|
-
const entrypoint = selectedModule
|
|
148
|
-
? resolveGeneratedModule(packageEntrypoint, selectedModule)
|
|
149
|
-
: packageEntrypoint;
|
|
150
|
-
const functions = publicFunctionExports(entrypoint);
|
|
151
|
-
if (functions.length === 0) {
|
|
152
|
-
throw new Error(`${config.entrypoint} exports no plain functions that can be bound to Python`);
|
|
153
|
-
}
|
|
154
|
-
const functionTypes = pythonFunctionTypes(entrypoint, functions);
|
|
155
|
-
applyOptionDefaults(functionTypes.records, await import(pathToFileURL(entrypoint).href));
|
|
156
|
-
const pythonFunctions = functions.map(({ name, sourceFile, sourceName, async }) => ({
|
|
157
|
-
javascriptName: name,
|
|
158
|
-
pythonName: pythonFunctionName(name),
|
|
159
|
-
async,
|
|
160
|
-
...(functionTypes.functions.get(`${sourceFile}#${sourceName}`) ?? {
|
|
161
|
-
parameters: [],
|
|
162
|
-
returnType: "Any",
|
|
163
|
-
}),
|
|
164
|
-
}));
|
|
165
|
-
const functionsByPythonName = new Map<string, string>();
|
|
166
|
-
for (const functionName of pythonFunctions) {
|
|
167
|
-
const existing = functionsByPythonName.get(functionName.pythonName);
|
|
168
|
-
if (existing && existing !== functionName.javascriptName) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`Python function name collision: ${existing} and ${functionName.javascriptName} both map to ${functionName.pythonName}`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
functionsByPythonName.set(functionName.pythonName, functionName.javascriptName);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const shimRoot = PYTHON_NODE_SHIM_ROOT;
|
|
177
|
-
const overridesByModule = groupOverrides(
|
|
178
|
-
configs.flatMap(({ functionOverrides }) => functionOverrides),
|
|
179
|
-
);
|
|
180
|
-
const shimEntry = "dbx-tools:python-entry";
|
|
181
|
-
const functionOverrideNamespace = "dbx-tools-function-override";
|
|
182
|
-
const shimAliases = new Map([
|
|
183
|
-
["child_process", resolve(shimRoot, "child-process.ts")],
|
|
184
|
-
["crypto", resolve(shimRoot, "crypto.ts")],
|
|
185
|
-
["fs", resolve(shimRoot, "fs.ts")],
|
|
186
|
-
["fs/promises", resolve(shimRoot, "fs-promises.ts")],
|
|
187
|
-
["os", resolve(shimRoot, "os.ts")],
|
|
188
|
-
["process", resolve(shimRoot, "process.ts")],
|
|
189
|
-
["readline", resolve(shimRoot, "readline.ts")],
|
|
190
|
-
]);
|
|
191
|
-
const bridgeSource = [
|
|
192
|
-
"export const __pythonGet = (target, name) => target[name];",
|
|
193
|
-
"export const __pythonInvokePositioned = (fn, entries) => {",
|
|
194
|
-
" const args = [];",
|
|
195
|
-
" for (const [index, value] of entries) args[index] = value;",
|
|
196
|
-
" return fn(...args);",
|
|
197
|
-
"};",
|
|
198
|
-
"export const __pythonKind = (value) => {",
|
|
199
|
-
" if (value === null) return 'null';",
|
|
200
|
-
" if (Array.isArray(value)) return 'array';",
|
|
201
|
-
" if (typeof value !== 'object') return typeof value;",
|
|
202
|
-
" const prototype = Object.getPrototypeOf(value);",
|
|
203
|
-
" return prototype === Object.prototype || prototype === null ? 'record' : 'instance';",
|
|
204
|
-
"};",
|
|
205
|
-
"export const __pythonInvokeMethod = async (target, name, args) => {",
|
|
206
|
-
" try {",
|
|
207
|
-
" const value = await Reflect.apply(target[name], target, args);",
|
|
208
|
-
" return { ok: true, value: value === undefined ? null : value };",
|
|
209
|
-
" } catch (error) {",
|
|
210
|
-
" return {",
|
|
211
|
-
" ok: false,",
|
|
212
|
-
" error: {",
|
|
213
|
-
" name: error instanceof Error ? error.name : 'Error',",
|
|
214
|
-
" message: error instanceof Error ? error.message : String(error),",
|
|
215
|
-
" stack: error instanceof Error ? error.stack : undefined,",
|
|
216
|
-
" },",
|
|
217
|
-
" };",
|
|
218
|
-
" }",
|
|
219
|
-
"};",
|
|
220
|
-
].join("\n");
|
|
221
|
-
const runtimeModules = configs.flatMap((binding) => {
|
|
222
|
-
const entrypoint = Bun.resolveSync(binding.entrypoint, binding.projectDirectory);
|
|
223
|
-
return binding.modules.length > 0
|
|
224
|
-
? binding.modules.map(
|
|
225
|
-
(module) => ({
|
|
226
|
-
key: runtimeModuleName(binding, module),
|
|
227
|
-
entrypoint: resolveGeneratedModule(entrypoint, module),
|
|
228
|
-
}),
|
|
229
|
-
)
|
|
230
|
-
: [{ key: runtimeModuleName(binding), entrypoint }];
|
|
231
|
-
});
|
|
232
|
-
const runtimeExports = [
|
|
233
|
-
...runtimeModules.map(
|
|
234
|
-
({ entrypoint }, index) =>
|
|
235
|
-
`import * as __pythonModule${index} from ${JSON.stringify(entrypoint)};`,
|
|
236
|
-
),
|
|
237
|
-
"const __pythonModules = {",
|
|
238
|
-
...runtimeModules.map(
|
|
239
|
-
({ key }, index) => ` ${JSON.stringify(key)}: () => __pythonModule${index},`,
|
|
240
|
-
),
|
|
241
|
-
"};",
|
|
242
|
-
"export const __pythonModule = (name) => {",
|
|
243
|
-
" const load = __pythonModules[name];",
|
|
244
|
-
" if (!load) throw new Error(`Unknown Python binding module: ${name}`);",
|
|
245
|
-
" return load();",
|
|
246
|
-
"};",
|
|
247
|
-
];
|
|
248
|
-
const standardAliases = stdLibBrowser as Record<string, string | undefined>;
|
|
249
|
-
const runtimePlugin: BunPlugin = {
|
|
250
|
-
name: "python-node-runtime",
|
|
251
|
-
setup(build) {
|
|
252
|
-
build.onResolve({ filter: /^dbx-tools:python-entry$/ }, () => ({
|
|
253
|
-
path: shimEntry,
|
|
254
|
-
namespace: "dbx-tools-python",
|
|
255
|
-
}));
|
|
256
|
-
build.onLoad({ filter: /.*/, namespace: "dbx-tools-python" }, () => ({
|
|
257
|
-
contents: [
|
|
258
|
-
`import ${JSON.stringify(resolve(shimRoot, "bootstrap.ts"))};`,
|
|
259
|
-
bridgeSource,
|
|
260
|
-
...runtimeExports,
|
|
261
|
-
].join("\n"),
|
|
262
|
-
loader: "ts",
|
|
263
|
-
}));
|
|
264
|
-
build.onLoad({ filter: /.*/, namespace: functionOverrideNamespace }, ({ path }) => {
|
|
265
|
-
const overrides = overridesByModule.get(path);
|
|
266
|
-
if (!overrides) throw new Error(`Missing function overrides for ${path}`);
|
|
267
|
-
const realModule = Bun.resolveSync(path, dirname(entrypoint));
|
|
268
|
-
return {
|
|
269
|
-
contents: [
|
|
270
|
-
`export * from ${JSON.stringify(realModule)};`,
|
|
271
|
-
...overrides.map(
|
|
272
|
-
(override) =>
|
|
273
|
-
`export { ${override.handlerExport} as ${override.targetExport} } from ${JSON.stringify(override.handlerFile)};`,
|
|
274
|
-
),
|
|
275
|
-
].join("\n"),
|
|
276
|
-
loader: "ts",
|
|
277
|
-
};
|
|
278
|
-
});
|
|
279
|
-
build.onResolve({ filter: /.*/ }, ({ path }) => {
|
|
280
|
-
if (path === "zod") {
|
|
281
|
-
return {
|
|
282
|
-
path: resolve(dirname(Bun.resolveSync("zod/package.json", root)), "index.cjs"),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
if (overridesByModule.has(path)) {
|
|
286
|
-
return { path, namespace: functionOverrideNamespace };
|
|
287
|
-
}
|
|
288
|
-
const moduleName = path.replace(/^node:/, "");
|
|
289
|
-
const shim = shimAliases.get(moduleName) ?? shimAliases.get(path);
|
|
290
|
-
if (shim) return { path: shim };
|
|
291
|
-
const standard = standardAliases[moduleName];
|
|
292
|
-
return standard
|
|
293
|
-
? { path: Bun.resolveSync(standard, dirname(fileURLToPath(import.meta.url))) }
|
|
294
|
-
: undefined;
|
|
295
|
-
});
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const runtimeSource = `${configs.map(({ entrypoint }) => entrypoint).join(", ")} configured by ${relative(root, pyproject)}`;
|
|
300
|
-
const source = `${config.entrypoint}${selectedModule ? `#${selectedModule}` : ""} configured by ${relative(root, pyproject)}`;
|
|
301
|
-
if (selectedBinding === undefined) {
|
|
302
|
-
prepareBindingDirectory(configs);
|
|
303
|
-
writeRuntimeLoader(configs);
|
|
304
|
-
}
|
|
305
|
-
await generate();
|
|
306
|
-
|
|
307
|
-
function discoverBindingModules(
|
|
308
|
-
candidate: ResolvedPythonNodeBindings,
|
|
309
|
-
): ResolvedPythonNodeBindings {
|
|
310
|
-
if (candidate.modules.length > 0) return candidate;
|
|
311
|
-
const packageEntrypoint = Bun.resolveSync(candidate.entrypoint, candidate.projectDirectory);
|
|
312
|
-
const modules = publicNamespaceExports(packageEntrypoint).filter(
|
|
313
|
-
(module) => publicFunctionExports(resolveGeneratedModule(packageEntrypoint, module)).length > 0,
|
|
314
|
-
);
|
|
315
|
-
return { ...candidate, modules };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function generate(): Promise<void> {
|
|
319
|
-
const result = await Bun.build({
|
|
320
|
-
entrypoints: [shimEntry],
|
|
321
|
-
format: "cjs",
|
|
322
|
-
plugins: [runtimePlugin],
|
|
323
|
-
target: "browser",
|
|
324
|
-
});
|
|
325
|
-
if (!result.success) {
|
|
326
|
-
for (const message of result.logs) logger.error(message);
|
|
327
|
-
throw new Error(`Could not bundle ${config.entrypoint}`);
|
|
328
|
-
}
|
|
329
|
-
if (result.outputs.length !== 1) {
|
|
330
|
-
throw new Error(`Expected one JavaScript bundle, received ${result.outputs.length}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const bundled = (await result.outputs[0].text()).replace(/[ \t]+$/gm, "");
|
|
334
|
-
const runtime = `${header({
|
|
335
|
-
tool: "projen/tasks/python-node-bindings.ts",
|
|
336
|
-
source: runtimeSource,
|
|
337
|
-
})}\n${bundled}`;
|
|
338
|
-
const bindings = pythonBindings(
|
|
339
|
-
source,
|
|
340
|
-
pythonFunctions,
|
|
341
|
-
functionTypes.records,
|
|
342
|
-
functionTypes.responses,
|
|
343
|
-
functionTypes.protocols,
|
|
344
|
-
runtimeModuleName(config, selectedModule),
|
|
345
|
-
);
|
|
346
|
-
writeGenerated(runtimeOutput, runtime, "JavaScript runtime");
|
|
347
|
-
writeGenerated(bindingsOutput, bindings, "Python bindings");
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function prepareBindingDirectory(bindings: readonly ResolvedPythonNodeBindings[]): void {
|
|
351
|
-
const moduleDirectory = bindings[0]?.moduleDirectory;
|
|
352
|
-
if (!moduleDirectory) return;
|
|
353
|
-
const nodeDirectory = join(moduleDirectory, "_generated", "node");
|
|
354
|
-
const generatedPackage = join(moduleDirectory, "_generated", "__init__.py");
|
|
355
|
-
if (values.check && existsSync(generatedPackage)) {
|
|
356
|
-
throw new Error(`Generated Node bindings contain stale file ${relative(root, generatedPackage)}`);
|
|
357
|
-
}
|
|
358
|
-
if (!values.check) rmSync(generatedPackage, { force: true });
|
|
359
|
-
const expected = new Set(
|
|
360
|
-
[
|
|
361
|
-
bindings[0]?.runtimeOutput,
|
|
362
|
-
moduleDirectory ? join(moduleDirectory, "_generated", "node", "_runtime.py") : undefined,
|
|
363
|
-
...bindings.flatMap((binding) => [
|
|
364
|
-
...(binding.modules.length > 0
|
|
365
|
-
? binding.modules.map((module) =>
|
|
366
|
-
join(binding.bindingDirectory, `${pythonFunctionName(module)}.py`),
|
|
367
|
-
)
|
|
368
|
-
: [join(binding.bindingDirectory, "index.py")]),
|
|
369
|
-
]),
|
|
370
|
-
].filter((file): file is string => Boolean(file)),
|
|
371
|
-
);
|
|
372
|
-
const stale = generatedFiles(nodeDirectory).filter((file) => !expected.has(file));
|
|
373
|
-
if (stale.length > 0) {
|
|
374
|
-
if (values.check) {
|
|
375
|
-
throw new Error(
|
|
376
|
-
`Generated Node bindings contain stale files:\n${stale.map((file) => ` ${relative(root, file)}`).join("\n")}`,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
for (const file of stale) rmSync(file, { force: true });
|
|
380
|
-
removeEmptyDirectories(nodeDirectory);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function writeRuntimeLoader(bindings: readonly ResolvedPythonNodeBindings[]): void {
|
|
385
|
-
const moduleDirectory = bindings[0]?.moduleDirectory;
|
|
386
|
-
if (!moduleDirectory) return;
|
|
387
|
-
writeGenerated(
|
|
388
|
-
join(moduleDirectory, "_generated", "node", "_runtime.py"),
|
|
389
|
-
pythonRuntimeLoader(
|
|
390
|
-
`${bindings.map(({ entrypoint }) => entrypoint).join(", ")} configured by ${relative(root, bindings[0]!.pyproject)}`,
|
|
391
|
-
),
|
|
392
|
-
"PythonMonkey runtime loader",
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function groupOverrides(
|
|
397
|
-
overrides: readonly FunctionOverride[],
|
|
398
|
-
): ReadonlyMap<string, readonly FunctionOverride[]> {
|
|
399
|
-
const grouped = new Map<string, FunctionOverride[]>();
|
|
400
|
-
for (const override of overrides) {
|
|
401
|
-
const current = grouped.get(override.targetModule) ?? [];
|
|
402
|
-
if (current.some((candidate) => candidate.targetExport === override.targetExport)) {
|
|
403
|
-
throw new Error(
|
|
404
|
-
`Duplicate function override for ${override.targetModule}#${override.targetExport}`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
current.push(override);
|
|
408
|
-
grouped.set(override.targetModule, current);
|
|
409
|
-
}
|
|
410
|
-
return grouped;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function pythonFunctionName(javascriptName: string): string {
|
|
414
|
-
const name = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, javascriptName);
|
|
415
|
-
if (!/^[_A-Za-z]\w*$/.test(name) || PYTHON_KEYWORDS.has(name)) {
|
|
416
|
-
throw new Error(
|
|
417
|
-
`JavaScript export ${javascriptName} does not map to a valid Python function name`,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
return name;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function runtimeModuleName(
|
|
424
|
-
binding: ResolvedPythonNodeBindings,
|
|
425
|
-
module?: string,
|
|
426
|
-
): string {
|
|
427
|
-
return [binding.bindingName, ...(module ? [pythonFunctionName(module)] : [])].join("__");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function pythonBindings(
|
|
431
|
-
source: string,
|
|
432
|
-
functions: readonly PythonFunctionBinding[],
|
|
433
|
-
records: readonly PythonRecord[],
|
|
434
|
-
responses: readonly PythonResponse[],
|
|
435
|
-
protocols: readonly PythonProtocol[],
|
|
436
|
-
javascriptModule: string,
|
|
437
|
-
): string {
|
|
438
|
-
const exported = [
|
|
439
|
-
...records.map(({ name }) => name),
|
|
440
|
-
...responses.map(({ name }) => name),
|
|
441
|
-
...protocols.map(({ name }) => name),
|
|
442
|
-
...functions.map(({ pythonName }) => pythonName),
|
|
443
|
-
]
|
|
444
|
-
.sort()
|
|
445
|
-
.map((name) => ` ${JSON.stringify(name)},`)
|
|
446
|
-
.join("\n");
|
|
447
|
-
const dataclasses = records.map(pythonDataclass).join("\n\n\n");
|
|
448
|
-
const responseTypes = orderPythonResponses(responses).map(pythonResponse).join("\n\n\n");
|
|
449
|
-
const protocolTypes = protocols.map(pythonProtocol).join("\n\n\n");
|
|
450
|
-
const wrappers = functions
|
|
451
|
-
.map(({ async, javascriptName, pythonName, parameters, returnType }) =>
|
|
452
|
-
pythonWrapper(
|
|
453
|
-
async,
|
|
454
|
-
javascriptModule,
|
|
455
|
-
javascriptName,
|
|
456
|
-
pythonName,
|
|
457
|
-
parameters,
|
|
458
|
-
returnType,
|
|
459
|
-
),
|
|
460
|
-
)
|
|
461
|
-
.join("\n\n\n");
|
|
462
|
-
const usesCallable = [dataclasses, responseTypes, protocolTypes, wrappers].some((section) =>
|
|
463
|
-
section.includes("Callable["),
|
|
464
|
-
);
|
|
465
|
-
return [
|
|
466
|
-
"# GENERATED by projen/tasks/python-node-bindings.ts - DO NOT EDIT.",
|
|
467
|
-
`# Regenerated from ${source}.`,
|
|
468
|
-
"# Hand edits are overwritten; this file is read-only.",
|
|
469
|
-
"",
|
|
470
|
-
"from __future__ import annotations",
|
|
471
|
-
"",
|
|
472
|
-
...(usesCallable ? ["from collections.abc import Callable"] : []),
|
|
473
|
-
"from dataclasses import dataclass, field",
|
|
474
|
-
"from typing import Any, NotRequired, Protocol, TypedDict",
|
|
475
|
-
"",
|
|
476
|
-
"from .._runtime import MISSING as _MISSING",
|
|
477
|
-
"from .._runtime import invoke_positioned as _invoke_positioned",
|
|
478
|
-
"from .._runtime import invoke_positioned_sync as _invoke_positioned_sync",
|
|
479
|
-
"",
|
|
480
|
-
"",
|
|
481
|
-
dataclasses,
|
|
482
|
-
...(dataclasses ? ["", ""] : []),
|
|
483
|
-
responseTypes,
|
|
484
|
-
...(responseTypes ? ["", ""] : []),
|
|
485
|
-
protocolTypes,
|
|
486
|
-
...(protocolTypes ? ["", ""] : []),
|
|
487
|
-
wrappers,
|
|
488
|
-
"",
|
|
489
|
-
"",
|
|
490
|
-
"__all__ = [",
|
|
491
|
-
exported,
|
|
492
|
-
"]",
|
|
493
|
-
"",
|
|
494
|
-
].join("\n");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
interface PythonFunctionBinding {
|
|
498
|
-
readonly async: boolean;
|
|
499
|
-
readonly javascriptName: string;
|
|
500
|
-
readonly parameters: readonly PythonParameter[];
|
|
501
|
-
readonly pythonName: string;
|
|
502
|
-
readonly returnType: string;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
interface PythonParameter {
|
|
506
|
-
readonly javascriptName: string;
|
|
507
|
-
readonly pythonName: string;
|
|
508
|
-
readonly record?: string;
|
|
509
|
-
readonly required: boolean;
|
|
510
|
-
readonly type: string;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
interface PythonRecord {
|
|
514
|
-
readonly name: string;
|
|
515
|
-
readonly fields: readonly PythonField[];
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
interface PythonField {
|
|
519
|
-
readonly defaultValue?: unknown;
|
|
520
|
-
readonly javascriptName: string;
|
|
521
|
-
readonly pythonName: string;
|
|
522
|
-
readonly type: string;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
interface PythonResponse {
|
|
526
|
-
readonly fields: readonly PythonResponseField[];
|
|
527
|
-
readonly name: string;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
interface PythonResponseField {
|
|
531
|
-
readonly name: string;
|
|
532
|
-
readonly required: boolean;
|
|
533
|
-
readonly type: string;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Emit nested response types before the responses that reference them.
|
|
538
|
-
*
|
|
539
|
-
* Functional TypedDict declarations evaluate their field types immediately,
|
|
540
|
-
* unlike class annotations protected by `from __future__ import annotations`.
|
|
541
|
-
*/
|
|
542
|
-
function orderPythonResponses(responses: readonly PythonResponse[]): PythonResponse[] {
|
|
543
|
-
const ordered: PythonResponse[] = [];
|
|
544
|
-
const visited = new Set<string>();
|
|
545
|
-
const visiting = new Set<string>();
|
|
546
|
-
|
|
547
|
-
const visit = (response: PythonResponse): void => {
|
|
548
|
-
if (visited.has(response.name) || visiting.has(response.name)) return;
|
|
549
|
-
visiting.add(response.name);
|
|
550
|
-
for (const dependency of responses) {
|
|
551
|
-
if (
|
|
552
|
-
dependency.name !== response.name &&
|
|
553
|
-
response.fields.some(({ type }) =>
|
|
554
|
-
new RegExp(`\\b${dependency.name}\\b`).test(type),
|
|
555
|
-
)
|
|
556
|
-
) {
|
|
557
|
-
visit(dependency);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
visiting.delete(response.name);
|
|
561
|
-
visited.add(response.name);
|
|
562
|
-
ordered.push(response);
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
for (const response of responses) visit(response);
|
|
566
|
-
return ordered;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
interface PythonProtocol {
|
|
570
|
-
readonly methods: readonly PythonProtocolMethod[];
|
|
571
|
-
readonly name: string;
|
|
572
|
-
readonly properties: readonly PythonProtocolProperty[];
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
interface PythonProtocolMethod {
|
|
576
|
-
readonly name: string;
|
|
577
|
-
readonly parameters: readonly PythonProtocolParameter[];
|
|
578
|
-
readonly returnType: string;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
interface PythonProtocolParameter {
|
|
582
|
-
readonly name: string;
|
|
583
|
-
readonly required: boolean;
|
|
584
|
-
readonly type: string;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
interface PythonProtocolProperty {
|
|
588
|
-
readonly name: string;
|
|
589
|
-
readonly required: boolean;
|
|
590
|
-
readonly type: string;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
interface PythonFunctionType {
|
|
594
|
-
readonly parameters: readonly PythonParameter[];
|
|
595
|
-
readonly returnType: string;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function pythonFunctionTypes(
|
|
599
|
-
entrypoint: string,
|
|
600
|
-
functions: readonly { sourceFile: string; sourceName: string }[],
|
|
601
|
-
): {
|
|
602
|
-
functions: Map<string, PythonFunctionType>;
|
|
603
|
-
protocols: PythonProtocol[];
|
|
604
|
-
records: PythonRecord[];
|
|
605
|
-
responses: PythonResponse[];
|
|
606
|
-
} {
|
|
607
|
-
const program = ts.createProgram({
|
|
608
|
-
rootNames: [entrypoint],
|
|
609
|
-
options: {
|
|
610
|
-
allowImportingTsExtensions: true,
|
|
611
|
-
module: ts.ModuleKind.ESNext,
|
|
612
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
613
|
-
noEmit: true,
|
|
614
|
-
skipLibCheck: true,
|
|
615
|
-
strictNullChecks: true,
|
|
616
|
-
target: ts.ScriptTarget.ESNext,
|
|
617
|
-
},
|
|
618
|
-
});
|
|
619
|
-
const checker = program.getTypeChecker();
|
|
620
|
-
const records = new Map<string, PythonRecord>();
|
|
621
|
-
const responses = new Map<string, PythonResponse>();
|
|
622
|
-
const protocols = new Map<string, PythonProtocol>();
|
|
623
|
-
const typesByFunction = new Map<string, PythonFunctionType>();
|
|
624
|
-
for (const exported of functions) {
|
|
625
|
-
const sourceFile = program.getSourceFile(exported.sourceFile);
|
|
626
|
-
if (!sourceFile) throw new Error(`TypeScript did not load ${exported.sourceFile}`);
|
|
627
|
-
const declaration = findFunction(sourceFile, exported.sourceName);
|
|
628
|
-
const parameters = declaration.parameters.map((parameter) => {
|
|
629
|
-
if (!ts.isIdentifier(parameter.name)) {
|
|
630
|
-
throw new Error(`${exported.sourceName} uses an unsupported destructured parameter`);
|
|
631
|
-
}
|
|
632
|
-
const parameterType = withoutUndefined(checker.getTypeAtLocation(parameter));
|
|
633
|
-
const path = `${exported.sourceName}.${parameter.name.text}`;
|
|
634
|
-
const record = pythonRecordType(
|
|
635
|
-
checker,
|
|
636
|
-
parameterType,
|
|
637
|
-
records,
|
|
638
|
-
path,
|
|
639
|
-
declaredTypeName(parameter.type),
|
|
640
|
-
);
|
|
641
|
-
const required = !parameter.questionToken && !parameter.initializer;
|
|
642
|
-
return {
|
|
643
|
-
javascriptName: parameter.name.text,
|
|
644
|
-
pythonName: pythonFunctionName(parameter.name.text),
|
|
645
|
-
type: record?.name ?? pythonType(checker, parameterType, records, path),
|
|
646
|
-
...(record ? { record: record.name } : {}),
|
|
647
|
-
required,
|
|
648
|
-
};
|
|
649
|
-
});
|
|
650
|
-
const signature = checker.getSignatureFromDeclaration(declaration);
|
|
651
|
-
if (!signature) throw new Error(`Could not resolve signature for ${exported.sourceName}`);
|
|
652
|
-
const returnType = pythonReturnType(
|
|
653
|
-
checker,
|
|
654
|
-
declaration.type
|
|
655
|
-
? checker.getTypeFromTypeNode(declaration.type)
|
|
656
|
-
: signature.getReturnType(),
|
|
657
|
-
responses,
|
|
658
|
-
protocols,
|
|
659
|
-
`${exported.sourceName}.return`,
|
|
660
|
-
declaredTypeName(declaration.type),
|
|
661
|
-
);
|
|
662
|
-
typesByFunction.set(`${exported.sourceFile}#${exported.sourceName}`, {
|
|
663
|
-
parameters,
|
|
664
|
-
returnType,
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
return {
|
|
668
|
-
functions: typesByFunction,
|
|
669
|
-
protocols: [...protocols.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
|
670
|
-
records: [...records.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
|
671
|
-
responses: [...responses.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function findFunction(source: ts.SourceFile, name: string): ts.FunctionDeclaration {
|
|
676
|
-
let found: ts.FunctionDeclaration | undefined;
|
|
677
|
-
const visit = (node: ts.Node): void => {
|
|
678
|
-
if (ts.isFunctionDeclaration(node) && node.name?.text === name) found = node;
|
|
679
|
-
else ts.forEachChild(node, visit);
|
|
680
|
-
};
|
|
681
|
-
visit(source);
|
|
682
|
-
if (!found) throw new Error(`Could not find function ${name} in ${source.fileName}`);
|
|
683
|
-
return found;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function pythonRecord(
|
|
687
|
-
checker: ts.TypeChecker,
|
|
688
|
-
type: ts.Type,
|
|
689
|
-
records: Map<string, PythonRecord>,
|
|
690
|
-
path: string,
|
|
691
|
-
preferredName?: string,
|
|
692
|
-
): PythonRecord {
|
|
693
|
-
const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
|
|
694
|
-
const name = symbolName && symbolName !== "__type" ? symbolName : preferredName;
|
|
695
|
-
if (!name || name === "__type") throw new Error(`${path} must reference a named object type`);
|
|
696
|
-
const existing = records.get(name);
|
|
697
|
-
if (existing) return existing;
|
|
698
|
-
const placeholder: PythonRecord = { name, fields: [] };
|
|
699
|
-
records.set(name, placeholder);
|
|
700
|
-
const fields = checker.getPropertiesOfType(type).map((property) => {
|
|
701
|
-
const propertyType = withoutUndefined(
|
|
702
|
-
resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
|
|
703
|
-
);
|
|
704
|
-
return {
|
|
705
|
-
javascriptName: property.name,
|
|
706
|
-
pythonName: pythonFunctionName(property.name),
|
|
707
|
-
type: pythonType(checker, propertyType, records, `${path}.${property.name}`),
|
|
708
|
-
};
|
|
709
|
-
});
|
|
710
|
-
const record = { name, fields };
|
|
711
|
-
records.set(name, record);
|
|
712
|
-
return record;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function pythonRecordType(
|
|
716
|
-
checker: ts.TypeChecker,
|
|
717
|
-
type: ts.Type,
|
|
718
|
-
records: Map<string, PythonRecord>,
|
|
719
|
-
path: string,
|
|
720
|
-
preferredName?: string,
|
|
721
|
-
): PythonRecord | undefined {
|
|
722
|
-
if (!(type.flags & ts.TypeFlags.Object)) return undefined;
|
|
723
|
-
if (checker.isArrayType(type) || type.getCallSignatures().length > 0) return undefined;
|
|
724
|
-
if (checker.getIndexTypeOfType(type, ts.IndexKind.String)) return undefined;
|
|
725
|
-
const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
|
|
726
|
-
const name = symbolName && symbolName !== "__type" ? symbolName : preferredName;
|
|
727
|
-
if (!name || name === "__type") return undefined;
|
|
728
|
-
return pythonRecord(checker, type, records, path, preferredName);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function declaredTypeName(node: ts.TypeNode | undefined): string | undefined {
|
|
732
|
-
if (!node) return undefined;
|
|
733
|
-
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
|
734
|
-
return node.typeName.text;
|
|
735
|
-
}
|
|
736
|
-
return undefined;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function pythonType(
|
|
740
|
-
checker: ts.TypeChecker,
|
|
741
|
-
type: ts.Type,
|
|
742
|
-
records: Map<string, PythonRecord>,
|
|
743
|
-
path: string,
|
|
744
|
-
): string {
|
|
745
|
-
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return "Any";
|
|
746
|
-
if (type.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Null))
|
|
747
|
-
{return "None";}
|
|
748
|
-
if (type.flags & ts.TypeFlags.StringLike) return "str";
|
|
749
|
-
if (type.flags & ts.TypeFlags.NumberLike) return "int | float";
|
|
750
|
-
if (type.flags & ts.TypeFlags.BooleanLike) return "bool";
|
|
751
|
-
if (type.isUnion()) {
|
|
752
|
-
const mapped = type.types
|
|
753
|
-
.filter((item) => !(item.flags & ts.TypeFlags.Undefined))
|
|
754
|
-
.map((item) => pythonType(checker, item, records, path));
|
|
755
|
-
return pythonUnion(mapped);
|
|
756
|
-
}
|
|
757
|
-
if (checker.isArrayType(type)) {
|
|
758
|
-
const element = checker.getTypeArguments(type as ts.TypeReference)[0];
|
|
759
|
-
if (!element) throw new Error(`${path} array element type could not be resolved`);
|
|
760
|
-
return `list[${pythonType(checker, withoutUndefined(element), records, `${path}[]`)}]`;
|
|
761
|
-
}
|
|
762
|
-
if (type.getCallSignatures().length > 0) {
|
|
763
|
-
return "Callable[..., Any]";
|
|
764
|
-
}
|
|
765
|
-
const stringIndex = checker.getIndexTypeOfType(type, ts.IndexKind.String);
|
|
766
|
-
if (stringIndex) {
|
|
767
|
-
return `dict[str, ${pythonType(checker, withoutUndefined(stringIndex), records, `${path}{}`)}]`;
|
|
768
|
-
}
|
|
769
|
-
const record = pythonRecordType(checker, type, records, path);
|
|
770
|
-
if (record) return record.name;
|
|
771
|
-
throw new Error(`${path} uses unsupported TypeScript type ${checker.typeToString(type)}`);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function pythonReturnType(
|
|
775
|
-
checker: ts.TypeChecker,
|
|
776
|
-
type: ts.Type,
|
|
777
|
-
responses: Map<string, PythonResponse>,
|
|
778
|
-
protocols: Map<string, PythonProtocol>,
|
|
779
|
-
path: string,
|
|
780
|
-
preferredName?: string,
|
|
781
|
-
): string {
|
|
782
|
-
if (type.isUnion()) {
|
|
783
|
-
return pythonUnion(
|
|
784
|
-
type.types.map((item) => pythonReturnType(checker, item, responses, protocols, path)),
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
|
|
788
|
-
const targetName = (type as ts.TypeReference).target?.getSymbol()?.getName();
|
|
789
|
-
if ([symbolName, targetName].some((name) => ["Promise", "PromiseLike"].includes(name ?? ""))) {
|
|
790
|
-
const [resolved] = checker.getTypeArguments(type as ts.TypeReference);
|
|
791
|
-
if (!resolved) throw new Error(`${path} promise type could not be resolved`);
|
|
792
|
-
return pythonReturnType(checker, resolved, responses, protocols, path);
|
|
793
|
-
}
|
|
794
|
-
const awaited = checker.getAwaitedType(type) ?? type;
|
|
795
|
-
if (awaited !== type) return pythonReturnType(checker, awaited, responses, protocols, path);
|
|
796
|
-
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return "Any";
|
|
797
|
-
if (type.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Null))
|
|
798
|
-
{return "None";}
|
|
799
|
-
if (type.flags & ts.TypeFlags.StringLike) return "str";
|
|
800
|
-
if (type.flags & ts.TypeFlags.NumberLike) return "int | float";
|
|
801
|
-
if (type.flags & ts.TypeFlags.BooleanLike) return "bool";
|
|
802
|
-
if (checker.isArrayType(type)) {
|
|
803
|
-
const element = checker.getTypeArguments(type as ts.TypeReference)[0];
|
|
804
|
-
if (!element) throw new Error(`${path} array element type could not be resolved`);
|
|
805
|
-
return `list[${pythonReturnType(checker, element, responses, protocols, `${path}[]`)}]`;
|
|
806
|
-
}
|
|
807
|
-
if ([symbolName, targetName].some((name) => ["Map", "ReadonlyMap"].includes(name ?? ""))) {
|
|
808
|
-
const [key, value] = checker.getTypeArguments(type as ts.TypeReference);
|
|
809
|
-
if (!key || !value) throw new Error(`${path} map types could not be resolved`);
|
|
810
|
-
return `dict[${pythonReturnType(checker, key, responses, protocols, `${path}.key`)}, ${pythonReturnType(checker, value, responses, protocols, `${path}.value`)}]`;
|
|
811
|
-
}
|
|
812
|
-
if (type.getCallSignatures().length > 0) return "Callable[..., Any]";
|
|
813
|
-
const stringIndex = checker.getIndexTypeOfType(type, ts.IndexKind.String);
|
|
814
|
-
if (stringIndex) {
|
|
815
|
-
return `dict[str, ${pythonReturnType(checker, stringIndex, responses, protocols, `${path}{}`)}]`;
|
|
816
|
-
}
|
|
817
|
-
if (!(type.flags & ts.TypeFlags.Object)) {
|
|
818
|
-
throw new Error(
|
|
819
|
-
`${path} uses unsupported TypeScript return type ${checker.typeToString(type)}`,
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
const properties = checker.getPropertiesOfType(type).filter(publicProperty);
|
|
823
|
-
const methods = properties.filter(
|
|
824
|
-
(property) =>
|
|
825
|
-
resolvedPropertyType(
|
|
826
|
-
checker,
|
|
827
|
-
type,
|
|
828
|
-
property,
|
|
829
|
-
`${path}.${property.name}`,
|
|
830
|
-
).getCallSignatures().length > 0,
|
|
831
|
-
);
|
|
832
|
-
if (methods.length > 0) {
|
|
833
|
-
const name = pythonObjectName(type, path, "Result");
|
|
834
|
-
if (protocols.has(name)) return name;
|
|
835
|
-
protocols.set(name, { name, methods: [], properties: [] });
|
|
836
|
-
const protocolMethods = methods.map((property) => {
|
|
837
|
-
const declaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
838
|
-
if (!declaration) throw new Error(`${path}.${property.name} has no TypeScript declaration`);
|
|
839
|
-
const signatures = checker
|
|
840
|
-
.getTypeOfSymbolAtLocation(property, declaration)
|
|
841
|
-
.getCallSignatures();
|
|
842
|
-
if (signatures.length !== 1) {
|
|
843
|
-
throw new Error(`${path}.${property.name} must have exactly one call signature`);
|
|
844
|
-
}
|
|
845
|
-
const signature = signatures[0];
|
|
846
|
-
const parameters = signature.getParameters().map((parameter) => {
|
|
847
|
-
const parameterDeclaration = parameter.valueDeclaration ?? parameter.declarations?.[0];
|
|
848
|
-
if (!parameterDeclaration) {
|
|
849
|
-
throw new Error(`${path}.${property.name}.${parameter.name} has no declaration`);
|
|
850
|
-
}
|
|
851
|
-
const parameterType = withoutUndefined(
|
|
852
|
-
checker.getTypeOfSymbolAtLocation(parameter, parameterDeclaration),
|
|
853
|
-
);
|
|
854
|
-
return {
|
|
855
|
-
name: pythonFunctionName(parameter.name),
|
|
856
|
-
required:
|
|
857
|
-
!(parameter.flags & ts.SymbolFlags.Optional) &&
|
|
858
|
-
(!ts.isParameter(parameterDeclaration) ||
|
|
859
|
-
(!parameterDeclaration.questionToken && !parameterDeclaration.initializer)),
|
|
860
|
-
type: pythonParameterReturnType(
|
|
861
|
-
checker,
|
|
862
|
-
parameterType,
|
|
863
|
-
responses,
|
|
864
|
-
protocols,
|
|
865
|
-
`${path}.${property.name}.${parameter.name}`,
|
|
866
|
-
),
|
|
867
|
-
};
|
|
868
|
-
});
|
|
869
|
-
const methodDeclaration = property.declarations?.find(
|
|
870
|
-
(candidate) => ts.isMethodDeclaration(candidate) || ts.isMethodSignature(candidate),
|
|
871
|
-
);
|
|
872
|
-
return {
|
|
873
|
-
name: pythonFunctionName(property.name),
|
|
874
|
-
parameters,
|
|
875
|
-
returnType: pythonReturnType(
|
|
876
|
-
checker,
|
|
877
|
-
methodDeclaration?.type
|
|
878
|
-
? checker.getTypeFromTypeNode(methodDeclaration.type)
|
|
879
|
-
: signature.getReturnType(),
|
|
880
|
-
responses,
|
|
881
|
-
protocols,
|
|
882
|
-
`${path}.${property.name}.return`,
|
|
883
|
-
),
|
|
884
|
-
};
|
|
885
|
-
});
|
|
886
|
-
const methodNames = new Set(methods.map(({ name }) => name));
|
|
887
|
-
const protocolProperties = properties
|
|
888
|
-
.filter(({ name }) => !methodNames.has(name))
|
|
889
|
-
.map((property) => {
|
|
890
|
-
return {
|
|
891
|
-
name: pythonFunctionName(property.name),
|
|
892
|
-
required: !(property.flags & ts.SymbolFlags.Optional),
|
|
893
|
-
type: pythonReturnType(
|
|
894
|
-
checker,
|
|
895
|
-
withoutUndefined(
|
|
896
|
-
resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
|
|
897
|
-
),
|
|
898
|
-
responses,
|
|
899
|
-
protocols,
|
|
900
|
-
`${path}.${property.name}`,
|
|
901
|
-
),
|
|
902
|
-
};
|
|
903
|
-
});
|
|
904
|
-
protocols.set(name, { name, methods: protocolMethods, properties: protocolProperties });
|
|
905
|
-
return name;
|
|
906
|
-
}
|
|
907
|
-
const name = `${preferredName ?? pythonObjectName(type, path, "Result")}Response`;
|
|
908
|
-
if (responses.has(name)) return name;
|
|
909
|
-
responses.set(name, { name, fields: [] });
|
|
910
|
-
const fields = properties.map((property) => {
|
|
911
|
-
return {
|
|
912
|
-
name: property.name,
|
|
913
|
-
required: !(property.flags & ts.SymbolFlags.Optional),
|
|
914
|
-
type: pythonReturnType(
|
|
915
|
-
checker,
|
|
916
|
-
withoutUndefined(
|
|
917
|
-
resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
|
|
918
|
-
),
|
|
919
|
-
responses,
|
|
920
|
-
protocols,
|
|
921
|
-
`${path}.${property.name}`,
|
|
922
|
-
),
|
|
923
|
-
};
|
|
924
|
-
});
|
|
925
|
-
responses.set(name, { name, fields });
|
|
926
|
-
return name;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function pythonParameterReturnType(
|
|
930
|
-
checker: ts.TypeChecker,
|
|
931
|
-
type: ts.Type,
|
|
932
|
-
responses: Map<string, PythonResponse>,
|
|
933
|
-
protocols: Map<string, PythonProtocol>,
|
|
934
|
-
path: string,
|
|
935
|
-
): string {
|
|
936
|
-
if (!type.isUnion()) return pythonReturnType(checker, type, responses, protocols, path);
|
|
937
|
-
return pythonUnion(
|
|
938
|
-
type.types
|
|
939
|
-
.filter((item) => !(item.flags & ts.TypeFlags.Undefined))
|
|
940
|
-
.map((item) => pythonReturnType(checker, item, responses, protocols, path)),
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function pythonUnion(types: readonly string[]): string {
|
|
945
|
-
return [...new Set(types)]
|
|
946
|
-
.sort((left, right) => (left === "None" ? 1 : right === "None" ? -1 : 0))
|
|
947
|
-
.join(" | ");
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function publicProperty(property: ts.Symbol): boolean {
|
|
951
|
-
if (!property.declarations?.length) return true;
|
|
952
|
-
return (property.declarations ?? []).some((declaration) => {
|
|
953
|
-
const flags = ts.getCombinedModifierFlags(declaration as ts.Declaration);
|
|
954
|
-
return !(flags & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected));
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function resolvedPropertyType(
|
|
959
|
-
checker: ts.TypeChecker,
|
|
960
|
-
owner: ts.Type,
|
|
961
|
-
property: ts.Symbol,
|
|
962
|
-
path: string,
|
|
963
|
-
): ts.Type {
|
|
964
|
-
const location =
|
|
965
|
-
property.valueDeclaration ??
|
|
966
|
-
property.declarations?.[0] ??
|
|
967
|
-
owner.aliasSymbol?.declarations?.[0] ??
|
|
968
|
-
owner.getSymbol()?.declarations?.[0];
|
|
969
|
-
if (!location) throw new Error(`${path} has no TypeScript type location`);
|
|
970
|
-
return checker.getTypeOfSymbolAtLocation(property, location);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function pythonObjectName(type: ts.Type, path: string, suffix: string): string {
|
|
974
|
-
const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
|
|
975
|
-
if (name && name !== "__type") return name;
|
|
976
|
-
const identifier = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, path);
|
|
977
|
-
return `${identifier
|
|
978
|
-
.split("_")
|
|
979
|
-
.filter(Boolean)
|
|
980
|
-
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
981
|
-
.join("")}${suffix}`;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function withoutUndefined(type: ts.Type): ts.Type {
|
|
985
|
-
if (!type.isUnion()) return type;
|
|
986
|
-
const retained = type.types.filter((candidate) => !(candidate.flags & ts.TypeFlags.Undefined));
|
|
987
|
-
if (retained.length !== 1) return type;
|
|
988
|
-
return retained[0];
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function pythonDataclass(record: PythonRecord): string {
|
|
992
|
-
const body = record.fields.length
|
|
993
|
-
? record.fields
|
|
994
|
-
.map((field) => {
|
|
995
|
-
const defaultArgument = Object.hasOwn(field, "defaultValue")
|
|
996
|
-
? pythonDefault(field.defaultValue)
|
|
997
|
-
: "default=None";
|
|
998
|
-
return [
|
|
999
|
-
` ${field.pythonName}: ${pythonOptionalType(field.type)} = field(`,
|
|
1000
|
-
` ${defaultArgument},`,
|
|
1001
|
-
` metadata={"javascript_name": ${JSON.stringify(field.javascriptName)}},`,
|
|
1002
|
-
" )",
|
|
1003
|
-
].join("\n");
|
|
1004
|
-
})
|
|
1005
|
-
.join("\n")
|
|
1006
|
-
: " pass";
|
|
1007
|
-
return `@dataclass(kw_only=True)\nclass ${record.name}:\n${body}`;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function pythonOptionalType(type: string): string {
|
|
1011
|
-
return type.split(" | ").includes("None") ? type : `${type} | None`;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function pythonResponse(response: PythonResponse): string {
|
|
1015
|
-
if (response.fields.some(({ name }) => !isPythonIdentifier(name))) {
|
|
1016
|
-
const fields = response.fields
|
|
1017
|
-
.map(
|
|
1018
|
-
({ name, required, type }) =>
|
|
1019
|
-
` ${JSON.stringify(name)}: ${required ? type : `NotRequired[${type}]`},`,
|
|
1020
|
-
)
|
|
1021
|
-
.join("\n");
|
|
1022
|
-
return [
|
|
1023
|
-
`${response.name} = TypedDict(`,
|
|
1024
|
-
` ${JSON.stringify(response.name)},`,
|
|
1025
|
-
" {",
|
|
1026
|
-
fields,
|
|
1027
|
-
" },",
|
|
1028
|
-
")",
|
|
1029
|
-
].join("\n");
|
|
1030
|
-
}
|
|
1031
|
-
const body = response.fields.length
|
|
1032
|
-
? response.fields
|
|
1033
|
-
.map(
|
|
1034
|
-
({ name, required, type }) =>
|
|
1035
|
-
` ${name}: ${required ? type : `NotRequired[${type}]`}`,
|
|
1036
|
-
)
|
|
1037
|
-
.join("\n")
|
|
1038
|
-
: " pass";
|
|
1039
|
-
return `class ${response.name}(TypedDict):\n${body}`;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function isPythonIdentifier(value: string): boolean {
|
|
1043
|
-
return /^[_A-Za-z]\w*$/.test(value) && !PYTHON_KEYWORDS.has(value);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
function pythonProtocol(protocol: PythonProtocol): string {
|
|
1047
|
-
const properties = protocol.properties.map(
|
|
1048
|
-
({ name, required, type }) => ` ${name}: ${required ? type : pythonOptionalType(type)}`,
|
|
1049
|
-
);
|
|
1050
|
-
const methods = protocol.methods.length
|
|
1051
|
-
? protocol.methods
|
|
1052
|
-
.map((method) => {
|
|
1053
|
-
const parameters = method.parameters.map(
|
|
1054
|
-
({ name, required, type }) => ` ${name}: ${type}${required ? "" : " = ..."},`,
|
|
1055
|
-
);
|
|
1056
|
-
return [
|
|
1057
|
-
` async def ${method.name}(`,
|
|
1058
|
-
" self,",
|
|
1059
|
-
...parameters,
|
|
1060
|
-
` ) -> ${method.returnType}: ...`,
|
|
1061
|
-
].join("\n");
|
|
1062
|
-
})
|
|
1063
|
-
.join("\n\n")
|
|
1064
|
-
: "";
|
|
1065
|
-
const body = [...properties, methods].filter(Boolean).join("\n\n") || " pass";
|
|
1066
|
-
return `class ${protocol.name}(Protocol):\n${body}`;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function applyOptionDefaults(
|
|
1070
|
-
records: readonly PythonRecord[],
|
|
1071
|
-
runtime: Record<string, unknown>,
|
|
1072
|
-
): void {
|
|
1073
|
-
for (const record of records) {
|
|
1074
|
-
const companion = runtime[record.name] as { defaults?: () => unknown } | undefined;
|
|
1075
|
-
if (typeof companion?.defaults !== "function") continue;
|
|
1076
|
-
const defaults = companion.defaults();
|
|
1077
|
-
if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
|
|
1078
|
-
throw new Error(`${record.name}.defaults() must return a record`);
|
|
1079
|
-
}
|
|
1080
|
-
const values = defaults as Record<string, unknown>;
|
|
1081
|
-
const fields = record.fields.map((field) =>
|
|
1082
|
-
Object.hasOwn(values, field.javascriptName)
|
|
1083
|
-
? { ...field, defaultValue: values[field.javascriptName] }
|
|
1084
|
-
: field,
|
|
1085
|
-
);
|
|
1086
|
-
(record as { fields: readonly PythonField[] }).fields = fields;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function pythonDefault(value: unknown): string {
|
|
1091
|
-
if (value === undefined) return "default=None";
|
|
1092
|
-
if (value === null) return "default=None";
|
|
1093
|
-
if (typeof value === "boolean") return `default=${value ? "True" : "False"}`;
|
|
1094
|
-
if (typeof value === "number" && Number.isFinite(value)) return `default=${value}`;
|
|
1095
|
-
if (typeof value === "string") return `default=${JSON.stringify(value)}`;
|
|
1096
|
-
if (Array.isArray(value) || (typeof value === "object" && value !== null)) {
|
|
1097
|
-
return `default_factory=lambda: ${pythonLiteral(value)}`;
|
|
1098
|
-
}
|
|
1099
|
-
throw new Error(`Unsupported defaults() value: ${String(value)}`);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function pythonLiteral(value: unknown): string {
|
|
1103
|
-
if (value === null) return "None";
|
|
1104
|
-
if (typeof value === "boolean") return value ? "True" : "False";
|
|
1105
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1106
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1107
|
-
if (Array.isArray(value)) return `[${value.map(pythonLiteral).join(", ")}]`;
|
|
1108
|
-
if (typeof value === "object") {
|
|
1109
|
-
return `{${Object.entries(value)
|
|
1110
|
-
.map(([key, item]) => `${JSON.stringify(key)}: ${pythonLiteral(item)}`)
|
|
1111
|
-
.join(", ")}}`;
|
|
1112
|
-
}
|
|
1113
|
-
throw new Error(`Unsupported defaults() value: ${String(value)}`);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function pythonWrapper(
|
|
1117
|
-
asyncFunction: boolean,
|
|
1118
|
-
javascriptModule: string,
|
|
1119
|
-
javascriptName: string,
|
|
1120
|
-
pythonName: string,
|
|
1121
|
-
parameters: readonly PythonParameter[],
|
|
1122
|
-
returnType: string,
|
|
1123
|
-
): string {
|
|
1124
|
-
const definition = asyncFunction ? "async def" : "def";
|
|
1125
|
-
const invoke = asyncFunction
|
|
1126
|
-
? `await _invoke_positioned(${JSON.stringify(javascriptModule)}, ${JSON.stringify(javascriptName)}, arguments)`
|
|
1127
|
-
: `_invoke_positioned_sync(${JSON.stringify(javascriptModule)}, ${JSON.stringify(javascriptName)}, arguments)`;
|
|
1128
|
-
if (parameters.length === 0) {
|
|
1129
|
-
return `${definition} ${pythonName}() -> ${returnType}:\n arguments: list[tuple[int, Any]] = []\n return ${invoke}`;
|
|
1130
|
-
}
|
|
1131
|
-
const signature = parameters.map((parameter) => {
|
|
1132
|
-
const type = parameter.record
|
|
1133
|
-
? `${parameter.record} | dict[str, Any] | None`
|
|
1134
|
-
: parameter.type;
|
|
1135
|
-
return ` ${parameter.pythonName}: ${type}${parameter.required ? "" : " | object = _MISSING"},`;
|
|
1136
|
-
});
|
|
1137
|
-
const body = [" arguments: list[tuple[int, Any]] = []"];
|
|
1138
|
-
for (const [index, parameter] of parameters.entries()) {
|
|
1139
|
-
if (parameter.required)
|
|
1140
|
-
{body.push(` arguments.append((${index}, ${parameter.pythonName}))`);}
|
|
1141
|
-
else {
|
|
1142
|
-
body.push(
|
|
1143
|
-
` if ${parameter.pythonName} is not _MISSING:`,
|
|
1144
|
-
` arguments.append((${index}, ${parameter.pythonName}))`,
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
body.push(` return ${invoke}`);
|
|
1149
|
-
return [`${definition} ${pythonName}(`, ...signature, `) -> ${returnType}:`, ...body].join("\n");
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function writeGenerated(output: string, contents: string, kind: string): void {
|
|
1153
|
-
const destination = relative(root, output);
|
|
1154
|
-
if (existsSync(output) && readFileSync(output, "utf8") === contents) {
|
|
1155
|
-
if (values.check) logger.info(`verified ${destination}`);
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
if (values.check) {
|
|
1159
|
-
throw new Error(`Generated ${kind} is stale: ${destination}`);
|
|
1160
|
-
}
|
|
1161
|
-
mkdirSync(dirname(output), { recursive: true });
|
|
1162
|
-
makeWritable(output);
|
|
1163
|
-
writeFileSync(output, contents);
|
|
1164
|
-
makeReadonly(output);
|
|
1165
|
-
logger.info(`generated ${destination}`);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
function generatedFiles(directory: string): string[] {
|
|
1170
|
-
if (!existsSync(directory)) return [];
|
|
1171
|
-
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
1172
|
-
if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) return [];
|
|
1173
|
-
const path = join(directory, entry.name);
|
|
1174
|
-
return entry.isDirectory() ? generatedFiles(path) : [path];
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function removeEmptyDirectories(directory: string): void {
|
|
1179
|
-
if (!existsSync(directory)) return;
|
|
1180
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1181
|
-
if (entry.isDirectory()) removeEmptyDirectories(join(directory, entry.name));
|
|
1182
|
-
}
|
|
1183
|
-
if (readdirSync(directory).length === 0) rmSync(directory, { recursive: true });
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function pythonRuntimeLoader(source: string): string {
|
|
1187
|
-
return [
|
|
1188
|
-
"# GENERATED by projen/tasks/python-node-bindings.ts - DO NOT EDIT.",
|
|
1189
|
-
`# Regenerated from ${source}.`,
|
|
1190
|
-
"# Hand edits are overwritten; this file is read-only.",
|
|
1191
|
-
"",
|
|
1192
|
-
"from __future__ import annotations",
|
|
1193
|
-
"",
|
|
1194
|
-
"import asyncio",
|
|
1195
|
-
"import inspect",
|
|
1196
|
-
"from dataclasses import fields, is_dataclass",
|
|
1197
|
-
"from pathlib import Path",
|
|
1198
|
-
"from threading import Lock",
|
|
1199
|
-
"from typing import Any",
|
|
1200
|
-
"",
|
|
1201
|
-
"import pythonmonkey as pm",
|
|
1202
|
-
"import pythonmonkey.require",
|
|
1203
|
-
"",
|
|
1204
|
-
"_RUNTIME: Any | None = None",
|
|
1205
|
-
"_LOCK = Lock()",
|
|
1206
|
-
"MISSING = object()",
|
|
1207
|
-
"",
|
|
1208
|
-
"",
|
|
1209
|
-
"def _load_runtime() -> Any:",
|
|
1210
|
-
" try:",
|
|
1211
|
-
" asyncio.get_running_loop()",
|
|
1212
|
-
" except RuntimeError:",
|
|
1213
|
-
" async def load() -> Any:",
|
|
1214
|
-
' return pm.require(str(Path(__file__).with_name("_runtime.js")))',
|
|
1215
|
-
"",
|
|
1216
|
-
" return asyncio.run(load())",
|
|
1217
|
-
' return pm.require(str(Path(__file__).with_name("_runtime.js")))',
|
|
1218
|
-
"",
|
|
1219
|
-
"",
|
|
1220
|
-
"def get_runtime() -> Any:",
|
|
1221
|
-
" global _RUNTIME",
|
|
1222
|
-
" if _RUNTIME is None:",
|
|
1223
|
-
" with _LOCK:",
|
|
1224
|
-
" if _RUNTIME is None:",
|
|
1225
|
-
" _RUNTIME = _load_runtime()",
|
|
1226
|
-
" return _RUNTIME",
|
|
1227
|
-
"",
|
|
1228
|
-
"",
|
|
1229
|
-
"def _module(name: str) -> Any:",
|
|
1230
|
-
' return get_runtime()["__pythonModule"](name)',
|
|
1231
|
-
"",
|
|
1232
|
-
"",
|
|
1233
|
-
"def _snake_to_camel(name: str) -> str:",
|
|
1234
|
-
' head, *tail = name.split("_")',
|
|
1235
|
-
' return head + "".join(part[:1].upper() + part[1:] for part in tail)',
|
|
1236
|
-
"",
|
|
1237
|
-
"",
|
|
1238
|
-
"def _to_javascript(value: Any) -> Any:",
|
|
1239
|
-
" if isinstance(value, _NodeObject):",
|
|
1240
|
-
" return value._target",
|
|
1241
|
-
" if is_dataclass(value) and not isinstance(value, type):",
|
|
1242
|
-
" return {",
|
|
1243
|
-
' item.metadata.get("javascript_name", item.name): _to_javascript(field_value)',
|
|
1244
|
-
" for item in fields(value)",
|
|
1245
|
-
" if (field_value := getattr(value, item.name)) is not None",
|
|
1246
|
-
" }",
|
|
1247
|
-
" if isinstance(value, dict):",
|
|
1248
|
-
" return {key: _to_javascript(item) for key, item in value.items()}",
|
|
1249
|
-
" if isinstance(value, (list, tuple)):",
|
|
1250
|
-
" return [_to_javascript(item) for item in value]",
|
|
1251
|
-
" return value",
|
|
1252
|
-
"",
|
|
1253
|
-
"",
|
|
1254
|
-
"def _from_javascript(value: Any) -> Any:",
|
|
1255
|
-
" if isinstance(value, str):",
|
|
1256
|
-
' return value.encode("utf-8").decode("utf-8")',
|
|
1257
|
-
" if value is None or isinstance(value, (int, float, bool)):",
|
|
1258
|
-
" return value",
|
|
1259
|
-
' kind = get_runtime()["__pythonKind"](value)',
|
|
1260
|
-
' if kind == "instance":',
|
|
1261
|
-
" return _NodeObject(value)",
|
|
1262
|
-
' if kind == "array":',
|
|
1263
|
-
" return [_from_javascript(item) for item in value]",
|
|
1264
|
-
' if kind == "record":',
|
|
1265
|
-
" return {str(key): _from_javascript(item) for key, item in value.items()}",
|
|
1266
|
-
" return value",
|
|
1267
|
-
"",
|
|
1268
|
-
"",
|
|
1269
|
-
"async def _resolve(value: Any) -> Any:",
|
|
1270
|
-
" if inspect.isawaitable(value):",
|
|
1271
|
-
" value = await value",
|
|
1272
|
-
" return _from_javascript(value)",
|
|
1273
|
-
"",
|
|
1274
|
-
"",
|
|
1275
|
-
"class _NodeObject:",
|
|
1276
|
-
" def __init__(self, target: Any) -> None:",
|
|
1277
|
-
" self._target = target",
|
|
1278
|
-
"",
|
|
1279
|
-
" def __getattr__(self, name: str) -> Any:",
|
|
1280
|
-
" javascript_name = _snake_to_camel(name)",
|
|
1281
|
-
' value = get_runtime()["__pythonGet"](self._target, javascript_name)',
|
|
1282
|
-
" if not callable(value):",
|
|
1283
|
-
" return _from_javascript(value)",
|
|
1284
|
-
"",
|
|
1285
|
-
" async def invoke(*args: Any) -> Any:",
|
|
1286
|
-
' result = await get_runtime()["__pythonInvokeMethod"](',
|
|
1287
|
-
" self._target,",
|
|
1288
|
-
" javascript_name,",
|
|
1289
|
-
" [_to_javascript(arg) for arg in args],",
|
|
1290
|
-
" )",
|
|
1291
|
-
' if result["ok"]:',
|
|
1292
|
-
' return _from_javascript(result["value"])',
|
|
1293
|
-
' error = result["error"]',
|
|
1294
|
-
" message = f\"{error['name']}: {error['message']}\"",
|
|
1295
|
-
' if error.get("stack"):',
|
|
1296
|
-
" message = f\"{message}\\n{error['stack']}\"",
|
|
1297
|
-
" raise RuntimeError(message)",
|
|
1298
|
-
"",
|
|
1299
|
-
" return invoke",
|
|
1300
|
-
"",
|
|
1301
|
-
"",
|
|
1302
|
-
"async def invoke_positioned(",
|
|
1303
|
-
" module: str,",
|
|
1304
|
-
" name: str,",
|
|
1305
|
-
" arguments: list[tuple[int, Any]],",
|
|
1306
|
-
") -> Any:",
|
|
1307
|
-
" return await _resolve(",
|
|
1308
|
-
' get_runtime()["__pythonInvokePositioned"](',
|
|
1309
|
-
" _module(module)[name],",
|
|
1310
|
-
" [[index, _to_javascript(value)] for index, value in arguments],",
|
|
1311
|
-
" ),",
|
|
1312
|
-
" )",
|
|
1313
|
-
"",
|
|
1314
|
-
"",
|
|
1315
|
-
"def invoke_positioned_sync(",
|
|
1316
|
-
" module: str,",
|
|
1317
|
-
" name: str,",
|
|
1318
|
-
" arguments: list[tuple[int, Any]],",
|
|
1319
|
-
") -> Any:",
|
|
1320
|
-
" return _from_javascript(",
|
|
1321
|
-
' get_runtime()["__pythonInvokePositioned"](',
|
|
1322
|
-
" _module(module)[name],",
|
|
1323
|
-
" [[index, _to_javascript(value)] for index, value in arguments],",
|
|
1324
|
-
" ),",
|
|
1325
|
-
" )",
|
|
1326
|
-
"",
|
|
1327
|
-
].join("\n");
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function resolveGeneratedModule(entrypoint: string, module: string): string {
|
|
1331
|
-
if (!/^[$A-Z_a-z][$\w]*$/.test(module)) {
|
|
1332
|
-
throw new Error(`Node binding module must be a TypeScript identifier: ${module}`);
|
|
1333
|
-
}
|
|
1334
|
-
const source = readFileSync(entrypoint, "utf8");
|
|
1335
|
-
const escaped = module.replaceAll(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
1336
|
-
const match = new RegExp(
|
|
1337
|
-
`export\\s+\\*\\s+as\\s+${escaped}\\s+from\\s+["']([^"']+)["']`,
|
|
1338
|
-
).exec(source);
|
|
1339
|
-
if (!match?.[1]) {
|
|
1340
|
-
throw new Error(`${entrypoint} does not export namespace ${module}`);
|
|
1341
|
-
}
|
|
1342
|
-
return Bun.resolveSync(match[1], dirname(entrypoint));
|
|
1343
|
-
}
|