tywrap 0.2.1 → 0.4.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/README.md +100 -32
- package/dist/cli.js +70 -5
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +62 -20
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +46 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +17 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -1
- package/dist/runtime/bridge-protocol.js +97 -0
- package/dist/runtime/bridge-protocol.js.map +1 -1
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts.map +1 -1
- package/dist/runtime/http-io.js.map +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -0
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +206 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +6 -5
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +6 -5
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +4 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +5 -0
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +64 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-io.d.ts +4 -5
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +99 -37
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +25 -3
- package/dist/runtime/safe-codec.d.ts.map +1 -1
- package/dist/runtime/safe-codec.js +231 -57
- package/dist/runtime/safe-codec.js.map +1 -1
- package/dist/runtime/transport.d.ts +6 -1
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +0 -11
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +13 -3
- package/dist/runtime/validators.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +17 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +97 -22
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +237 -331
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +10 -16
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +47 -21
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +85 -5
- package/src/config/index.ts +91 -32
- package/src/core/analyzer.ts +51 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/dev.ts +983 -0
- package/src/index.ts +35 -6
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +164 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +2 -1
- package/src/runtime/http.ts +7 -0
- package/src/runtime/node.ts +312 -46
- package/src/runtime/optimized-node.ts +9 -5
- package/src/runtime/pooled-transport.ts +12 -1
- package/src/runtime/process-io.ts +83 -55
- package/src/runtime/pyodide-io.ts +110 -44
- package/src/runtime/safe-codec.ts +290 -82
- package/src/runtime/transport.ts +6 -1
- package/src/runtime/validators.ts +14 -3
- package/src/runtime/worker-pool.ts +120 -24
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -8
- package/src/tywrap.ts +296 -361
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +11 -15
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
package/src/tywrap.ts
CHANGED
|
@@ -4,19 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { CodeGenerator } from './core/generator.js';
|
|
6
6
|
import { TypeMapper } from './core/mapper.js';
|
|
7
|
+
import { parseAnnotationToPythonType } from './core/annotation-parser.js';
|
|
7
8
|
import { createConfig } from './config/index.js';
|
|
8
9
|
import type {
|
|
9
10
|
TywrapOptions,
|
|
11
|
+
PythonGenericParameter,
|
|
10
12
|
PythonFunction,
|
|
11
13
|
PythonModule as TSPythonModule,
|
|
12
14
|
PythonClass,
|
|
15
|
+
PythonTypeAlias,
|
|
13
16
|
Parameter,
|
|
14
17
|
PythonType,
|
|
15
18
|
} from './types/index.js';
|
|
16
|
-
import { fsUtils, pathUtils, processUtils,
|
|
19
|
+
import { fsUtils, pathUtils, processUtils, isWindows } from './utils/runtime.js';
|
|
17
20
|
import { globalCache } from './utils/cache.js';
|
|
18
21
|
import { globalParallelProcessor } from './utils/parallel-processor.js';
|
|
19
22
|
import { resolvePythonExecutable } from './utils/python.js';
|
|
23
|
+
import { computeIrCacheFilename } from './utils/ir-cache.js';
|
|
24
|
+
|
|
25
|
+
const TYWRAP_IR_VERSION = '0.2.0';
|
|
20
26
|
|
|
21
27
|
// Collect unknown typing constructs encountered during annotation parsing (per-generate run)
|
|
22
28
|
let unknownTypeNamesCollector: Map<string, number> = new Map();
|
|
@@ -80,9 +86,16 @@ export interface GenerateRunOptions {
|
|
|
80
86
|
check?: boolean;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
export interface GenerateFailure {
|
|
90
|
+
module: string;
|
|
91
|
+
code: 'ir-unavailable';
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
export interface GenerateResult {
|
|
84
96
|
written: string[];
|
|
85
97
|
warnings: string[];
|
|
98
|
+
failures: GenerateFailure[];
|
|
86
99
|
/**
|
|
87
100
|
* Only set when `GenerateRunOptions.check === true`.
|
|
88
101
|
* Lists files that are missing or differ from what would be generated.
|
|
@@ -117,6 +130,7 @@ export async function generate(
|
|
|
117
130
|
const written: string[] = [];
|
|
118
131
|
const outOfDate: string[] = [];
|
|
119
132
|
const warnings: string[] = [];
|
|
133
|
+
const failures: GenerateFailure[] = [];
|
|
120
134
|
const outputDir = resolvedOptions.output.dir;
|
|
121
135
|
const caching = resolvedOptions.performance.caching;
|
|
122
136
|
const cacheDir = '.tywrap/cache';
|
|
@@ -138,13 +152,14 @@ export async function generate(
|
|
|
138
152
|
const modules = resolvedOptions.pythonModules ?? {};
|
|
139
153
|
for (const entry of Object.entries(modules)) {
|
|
140
154
|
const moduleKey = entry[0];
|
|
155
|
+
const moduleConfig = entry[1];
|
|
141
156
|
// reset collector for each module
|
|
142
157
|
unknownTypeNamesCollector = new Map();
|
|
143
158
|
// Prefer Python IR extractor over TS analyzer with optional cache
|
|
144
|
-
const cacheKey = await computeCacheKey(moduleKey,
|
|
159
|
+
const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
|
|
145
160
|
let ir: unknown | null = null;
|
|
146
161
|
let irError: string | undefined;
|
|
147
|
-
if (caching && fsUtils.isAvailable()) {
|
|
162
|
+
if (caching && fsUtils.isAvailable() && !checkMode) {
|
|
148
163
|
try {
|
|
149
164
|
const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
|
|
150
165
|
ir = JSON.parse(cached);
|
|
@@ -153,7 +168,10 @@ export async function generate(
|
|
|
153
168
|
}
|
|
154
169
|
}
|
|
155
170
|
if (!ir) {
|
|
156
|
-
const fetchResult = await fetchPythonIr(moduleKey, pythonPath
|
|
171
|
+
const fetchResult = await fetchPythonIr(moduleKey, pythonPath, {
|
|
172
|
+
timeoutMs: resolvedOptions.runtime?.node?.timeout,
|
|
173
|
+
pythonImportPath: resolvedOptions.pythonImportPath,
|
|
174
|
+
});
|
|
157
175
|
ir = fetchResult.ir;
|
|
158
176
|
irError = fetchResult.error;
|
|
159
177
|
if (ir && caching && fsUtils.isAvailable() && !checkMode) {
|
|
@@ -163,12 +181,90 @@ export async function generate(
|
|
|
163
181
|
}
|
|
164
182
|
}
|
|
165
183
|
if (!ir) {
|
|
166
|
-
|
|
184
|
+
const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
|
|
185
|
+
warnings.push(message);
|
|
186
|
+
failures.push({
|
|
187
|
+
module: moduleKey,
|
|
188
|
+
code: 'ir-unavailable',
|
|
189
|
+
message,
|
|
190
|
+
});
|
|
167
191
|
continue;
|
|
168
192
|
}
|
|
169
193
|
|
|
170
194
|
const moduleModel = transformIrToTsModel(ir);
|
|
171
195
|
|
|
196
|
+
// Apply module-level export filtering (functions/classes + excludes).
|
|
197
|
+
{
|
|
198
|
+
const builtInDefaultExcludes = new Set([
|
|
199
|
+
'dataclass',
|
|
200
|
+
'property',
|
|
201
|
+
'staticmethod',
|
|
202
|
+
'classmethod',
|
|
203
|
+
'abstractmethod',
|
|
204
|
+
'cached_property',
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
const excludeExact = new Set((moduleConfig.exclude ?? []).map(String));
|
|
208
|
+
const excludeRegexes: RegExp[] = [];
|
|
209
|
+
for (const pattern of moduleConfig.excludePatterns ?? []) {
|
|
210
|
+
try {
|
|
211
|
+
excludeRegexes.push(new RegExp(String(pattern)));
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
214
|
+
warnings.push(
|
|
215
|
+
`Module ${moduleKey}: invalid excludePatterns regex "${String(pattern)}": ${message}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const shouldExclude = (name: string, applyBuiltInDefaults: boolean): boolean => {
|
|
221
|
+
if (excludeExact.has(name)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (excludeRegexes.some(r => r.test(name))) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (applyBuiltInDefaults && builtInDefaultExcludes.has(name)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (Array.isArray(moduleConfig.functions)) {
|
|
234
|
+
const allow = new Set(moduleConfig.functions.map(String));
|
|
235
|
+
for (const requested of allow) {
|
|
236
|
+
if (!moduleModel.functions.some(f => f.name === requested)) {
|
|
237
|
+
warnings.push(
|
|
238
|
+
`Module ${moduleKey}: configured function "${requested}" not found in IR`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
moduleModel.functions = moduleModel.functions.filter(
|
|
243
|
+
f => allow.has(f.name) && !shouldExclude(f.name, false)
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
moduleModel.functions = moduleModel.functions.filter(f => !shouldExclude(f.name, true));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Array.isArray(moduleConfig.classes)) {
|
|
250
|
+
const allow = new Set(moduleConfig.classes.map(String));
|
|
251
|
+
for (const requested of allow) {
|
|
252
|
+
if (!moduleModel.classes.some(c => c.name === requested)) {
|
|
253
|
+
warnings.push(`Module ${moduleKey}: configured class "${requested}" not found in IR`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
moduleModel.classes = moduleModel.classes.filter(
|
|
257
|
+
c => allow.has(c.name) && !shouldExclude(c.name, false)
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
moduleModel.classes = moduleModel.classes.filter(c => !shouldExclude(c.name, true));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
moduleModel.typeAliases = (moduleModel.typeAliases ?? []).filter(
|
|
264
|
+
alias => !shouldExclude(alias.name, false)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
172
268
|
// Generate module code
|
|
173
269
|
const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
|
|
174
270
|
const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
|
|
@@ -182,7 +278,7 @@ export async function generate(
|
|
|
182
278
|
if (resolvedOptions.output?.declaration) {
|
|
183
279
|
filesToEmit.push({
|
|
184
280
|
path: pathUtils.join(outputDir, `${baseName}.generated.d.ts`),
|
|
185
|
-
content:
|
|
281
|
+
content: gen.declaration,
|
|
186
282
|
});
|
|
187
283
|
}
|
|
188
284
|
|
|
@@ -242,10 +338,10 @@ export async function generate(
|
|
|
242
338
|
}
|
|
243
339
|
|
|
244
340
|
if (checkMode) {
|
|
245
|
-
return { written: [], warnings, outOfDate };
|
|
341
|
+
return { written: [], warnings, failures, outOfDate };
|
|
246
342
|
}
|
|
247
343
|
|
|
248
|
-
return { written, warnings };
|
|
344
|
+
return { written, warnings, failures };
|
|
249
345
|
}
|
|
250
346
|
|
|
251
347
|
/**
|
|
@@ -253,19 +349,28 @@ export async function generate(
|
|
|
253
349
|
*/
|
|
254
350
|
async function fetchPythonIr(
|
|
255
351
|
moduleName: string,
|
|
256
|
-
pythonPath: string
|
|
352
|
+
pythonPath: string,
|
|
353
|
+
options: { timeoutMs?: number; pythonImportPath?: string[] } = {}
|
|
257
354
|
): Promise<{ ir: unknown | null; error?: string }> {
|
|
258
355
|
if (!processUtils.isAvailable()) {
|
|
259
356
|
return { ir: null, error: 'Subprocess operations not available in this runtime' };
|
|
260
357
|
}
|
|
358
|
+
const delimiter = isWindows() ? ';' : ':';
|
|
359
|
+
const extraPaths = (options.pythonImportPath ?? []).filter(Boolean);
|
|
360
|
+
const existingPyPath =
|
|
361
|
+
typeof process !== 'undefined' && typeof process.env === 'object' && process.env
|
|
362
|
+
? process.env.PYTHONPATH
|
|
363
|
+
: undefined;
|
|
364
|
+
const mergedPyPath = [...extraPaths, ...(existingPyPath ? [existingPyPath] : [])]
|
|
365
|
+
.filter(Boolean)
|
|
366
|
+
.join(delimiter);
|
|
367
|
+
const env = mergedPyPath ? { PYTHONPATH: mergedPyPath } : undefined;
|
|
261
368
|
try {
|
|
262
|
-
const result = await processUtils.exec(
|
|
263
|
-
|
|
264
|
-
'tywrap_ir',
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
'--no-pretty',
|
|
268
|
-
]);
|
|
369
|
+
const result = await processUtils.exec(
|
|
370
|
+
pythonPath,
|
|
371
|
+
['-m', 'tywrap_ir', '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
|
|
372
|
+
{ timeoutMs: options.timeoutMs, env }
|
|
373
|
+
);
|
|
269
374
|
if (result.code === 0) {
|
|
270
375
|
try {
|
|
271
376
|
return { ir: JSON.parse(result.stdout) };
|
|
@@ -277,18 +382,40 @@ async function fetchPythonIr(
|
|
|
277
382
|
}
|
|
278
383
|
}
|
|
279
384
|
|
|
280
|
-
|
|
385
|
+
const stderrText = result.stderr.trim();
|
|
386
|
+
const isTywrapIrMissing =
|
|
387
|
+
stderrText.includes('No module named') && stderrText.includes('tywrap_ir');
|
|
388
|
+
if (!isTywrapIrMissing) {
|
|
389
|
+
return { ir: null, error: `tywrap_ir failed. stderr: ${stderrText || 'empty'}` };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Fallback to invoking local __main__.py (useful when running from the repo).
|
|
281
393
|
const localMain = pathUtils.join(process.cwd(), 'tywrap_ir', 'tywrap_ir', '__main__.py');
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
394
|
+
let localMainExists = false;
|
|
395
|
+
if (fsUtils.isAvailable()) {
|
|
396
|
+
try {
|
|
397
|
+
await fsUtils.readFile(localMain);
|
|
398
|
+
localMainExists = true;
|
|
399
|
+
} catch {
|
|
400
|
+
localMainExists = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!localMainExists) {
|
|
404
|
+
return {
|
|
405
|
+
ir: null,
|
|
406
|
+
error: `tywrap_ir not found on PYTHONPATH. stderr: ${stderrText || 'empty'}`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const fallback = await processUtils.exec(
|
|
411
|
+
pythonPath,
|
|
412
|
+
[localMain, '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
|
|
413
|
+
{ timeoutMs: options.timeoutMs, env }
|
|
414
|
+
);
|
|
288
415
|
if (fallback.code !== 0) {
|
|
289
416
|
return {
|
|
290
417
|
ir: null,
|
|
291
|
-
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() ||
|
|
418
|
+
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || stderrText || 'empty'}`,
|
|
292
419
|
};
|
|
293
420
|
}
|
|
294
421
|
try {
|
|
@@ -307,73 +434,161 @@ async function fetchPythonIr(
|
|
|
307
434
|
/**
|
|
308
435
|
* Convert JSON IR from Python into the internal TypeScript model used by the generator.
|
|
309
436
|
*/
|
|
437
|
+
function collectModuleTypeVarNames(obj: Record<string, unknown>): Set<string> {
|
|
438
|
+
const names = new Set<string>();
|
|
439
|
+
const constants = Array.isArray(obj.constants) ? (obj.constants as unknown[]) : [];
|
|
440
|
+
|
|
441
|
+
for (const constant of constants) {
|
|
442
|
+
const entry = constant as Record<string, unknown>;
|
|
443
|
+
const name = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
444
|
+
if (!name) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const valueRepr = typeof entry.value_repr === 'string' ? entry.value_repr.trim() : '';
|
|
449
|
+
if (/^~?[A-Za-z_][A-Za-z0-9_]*$/.test(valueRepr) && valueRepr.replace(/^~/, '') === name) {
|
|
450
|
+
names.add(name);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const annotation = typeof entry.annotation === 'string' ? entry.annotation.trim() : '';
|
|
455
|
+
if (/(^|\.)(TypeVar|ParamSpec|TypeVarTuple)(\[|\(|$)/.test(annotation)) {
|
|
456
|
+
names.add(name);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return names;
|
|
461
|
+
}
|
|
462
|
+
|
|
310
463
|
function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
311
464
|
const obj: Record<string, unknown> =
|
|
312
465
|
typeof ir === 'object' && ir !== null ? (ir as Record<string, unknown>) : {};
|
|
313
466
|
const functions = (obj.functions as unknown[]) ?? [];
|
|
314
467
|
const classes = (obj.classes as unknown[]) ?? [];
|
|
315
|
-
const
|
|
468
|
+
const aliases = (obj.type_aliases as unknown[]) ?? [];
|
|
469
|
+
const moduleTypeVarNames = collectModuleTypeVarNames(obj);
|
|
470
|
+
const parseType = (
|
|
471
|
+
annotation: unknown,
|
|
472
|
+
typeParameters: readonly PythonGenericParameter[] = []
|
|
473
|
+
): PythonType =>
|
|
474
|
+
parseAnnotationToPythonType(annotation, {
|
|
475
|
+
onUnknownTypeName: recordUnknown,
|
|
476
|
+
knownTypeVarNames: moduleTypeVarNames,
|
|
477
|
+
typeParameters,
|
|
478
|
+
});
|
|
479
|
+
const mapTypeParameters = (value: Record<string, unknown>): PythonGenericParameter[] =>
|
|
480
|
+
Array.isArray(value.type_params)
|
|
481
|
+
? (value.type_params as unknown[]).map(v => {
|
|
482
|
+
const param = (v ?? {}) as Record<string, unknown>;
|
|
483
|
+
return {
|
|
484
|
+
name: String(param.name ?? ''),
|
|
485
|
+
kind: String(param.kind ?? 'typevar') as PythonGenericParameter['kind'],
|
|
486
|
+
bound: param.bound ? parseType(param.bound) : undefined,
|
|
487
|
+
constraints: Array.isArray(param.constraints)
|
|
488
|
+
? (param.constraints as unknown[]).map(item => parseType(item))
|
|
489
|
+
: undefined,
|
|
490
|
+
variance:
|
|
491
|
+
param.variance === 'covariant' ||
|
|
492
|
+
param.variance === 'contravariant' ||
|
|
493
|
+
param.variance === 'invariant'
|
|
494
|
+
? param.variance
|
|
495
|
+
: undefined,
|
|
496
|
+
} satisfies PythonGenericParameter;
|
|
497
|
+
})
|
|
498
|
+
: [];
|
|
499
|
+
const mapParam = (
|
|
500
|
+
p: Record<string, unknown>,
|
|
501
|
+
typeParameters: readonly PythonGenericParameter[] = []
|
|
502
|
+
): Parameter => ({
|
|
316
503
|
name: String(p.name ?? ''),
|
|
317
|
-
type:
|
|
504
|
+
type: parseType(p.annotation, typeParameters),
|
|
318
505
|
optional: Boolean(p.default),
|
|
319
506
|
varArgs: p.kind === 'VAR_POSITIONAL',
|
|
320
507
|
kwArgs: p.kind === 'VAR_KEYWORD',
|
|
508
|
+
positionalOnly: p.kind === 'POSITIONAL_ONLY',
|
|
509
|
+
keywordOnly: p.kind === 'KEYWORD_ONLY',
|
|
321
510
|
});
|
|
322
511
|
|
|
323
|
-
const mapFunc = (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
512
|
+
const mapFunc = (
|
|
513
|
+
f: Record<string, unknown>,
|
|
514
|
+
inheritedTypeParameters: readonly PythonGenericParameter[] = []
|
|
515
|
+
): PythonFunction => {
|
|
516
|
+
const localTypeParameters = mapTypeParameters(f);
|
|
517
|
+
const annotationTypeParameters = [...inheritedTypeParameters, ...localTypeParameters];
|
|
518
|
+
return {
|
|
519
|
+
name: String(f.name ?? ''),
|
|
520
|
+
signature: {
|
|
521
|
+
parameters: Array.isArray(f.parameters)
|
|
522
|
+
? (f.parameters as unknown[]).map(v =>
|
|
523
|
+
mapParam((v ?? {}) as Record<string, unknown>, annotationTypeParameters)
|
|
524
|
+
)
|
|
525
|
+
: [],
|
|
526
|
+
returnType: parseType(f.returns, annotationTypeParameters),
|
|
527
|
+
isAsync: Boolean(f.is_async),
|
|
528
|
+
isGenerator: Boolean(f.is_generator),
|
|
529
|
+
},
|
|
530
|
+
docstring: (f.docstring as string | undefined) ?? undefined,
|
|
531
|
+
decorators: [],
|
|
330
532
|
isAsync: Boolean(f.is_async),
|
|
331
533
|
isGenerator: Boolean(f.is_generator),
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
534
|
+
typeParameters: [...localTypeParameters],
|
|
535
|
+
returnType: parseType(f.returns, annotationTypeParameters),
|
|
536
|
+
parameters: Array.isArray(f.parameters)
|
|
537
|
+
? (f.parameters as unknown[]).map(v =>
|
|
538
|
+
mapParam((v ?? {}) as Record<string, unknown>, annotationTypeParameters)
|
|
539
|
+
)
|
|
540
|
+
: [],
|
|
541
|
+
};
|
|
542
|
+
};
|
|
342
543
|
|
|
343
|
-
const mapClass = (c: Record<string, unknown>): PythonClass =>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
? (c.
|
|
348
|
-
:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
: (c.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
544
|
+
const mapClass = (c: Record<string, unknown>): PythonClass => {
|
|
545
|
+
const classTypeParameters = mapTypeParameters(c);
|
|
546
|
+
return {
|
|
547
|
+
name: String(c.name ?? ''),
|
|
548
|
+
bases: Array.isArray(c.bases) ? (c.bases as string[]) : [],
|
|
549
|
+
methods: Array.isArray(c.methods)
|
|
550
|
+
? (c.methods as unknown[]).map(v =>
|
|
551
|
+
mapFunc((v ?? {}) as Record<string, unknown>, classTypeParameters)
|
|
552
|
+
)
|
|
553
|
+
: [],
|
|
554
|
+
properties: Array.isArray(c.fields)
|
|
555
|
+
? (c.fields as unknown[]).map(v => {
|
|
556
|
+
const p = (v ?? {}) as Record<string, unknown>;
|
|
557
|
+
return {
|
|
558
|
+
name: String(p.name ?? ''),
|
|
559
|
+
type: parseType(p.annotation, classTypeParameters),
|
|
560
|
+
readonly: false,
|
|
561
|
+
setter: false,
|
|
562
|
+
getter: true,
|
|
563
|
+
optional: Boolean(p.default),
|
|
564
|
+
};
|
|
565
|
+
})
|
|
566
|
+
: [],
|
|
567
|
+
docstring: (c.docstring as string | undefined) ?? undefined,
|
|
568
|
+
decorators: (c.typed_dict as boolean) ? ['__typed_dict__'] : [],
|
|
569
|
+
kind: (c.typed_dict as boolean)
|
|
570
|
+
? 'typed_dict'
|
|
571
|
+
: (c.is_protocol as boolean)
|
|
572
|
+
? 'protocol'
|
|
573
|
+
: (c.is_namedtuple as boolean)
|
|
574
|
+
? 'namedtuple'
|
|
575
|
+
: (c.is_dataclass as boolean)
|
|
576
|
+
? 'dataclass'
|
|
577
|
+
: (c.is_pydantic as boolean)
|
|
578
|
+
? 'pydantic'
|
|
579
|
+
: 'class',
|
|
580
|
+
typeParameters: classTypeParameters,
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const mapTypeAlias = (alias: Record<string, unknown>): PythonTypeAlias => {
|
|
585
|
+
const typeParameters = mapTypeParameters(alias);
|
|
586
|
+
return {
|
|
587
|
+
name: String(alias.name ?? ''),
|
|
588
|
+
type: parseType(alias.definition, typeParameters),
|
|
589
|
+
typeParameters,
|
|
590
|
+
};
|
|
591
|
+
};
|
|
377
592
|
|
|
378
593
|
const moduleModel: TSPythonModule = {
|
|
379
594
|
name: (obj.module as string) ?? 'module',
|
|
@@ -384,255 +599,13 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
384
599
|
: undefined,
|
|
385
600
|
functions: functions.map(v => mapFunc((v ?? {}) as Record<string, unknown>)),
|
|
386
601
|
classes: classes.map(v => mapClass((v ?? {}) as Record<string, unknown>)),
|
|
602
|
+
typeAliases: aliases.map(v => mapTypeAlias((v ?? {}) as Record<string, unknown>)),
|
|
387
603
|
imports: [],
|
|
388
604
|
exports: [],
|
|
389
605
|
};
|
|
390
606
|
return moduleModel;
|
|
391
607
|
}
|
|
392
608
|
|
|
393
|
-
function unknownType(): PythonType {
|
|
394
|
-
return { kind: 'custom', name: 'Any', module: 'typing' };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Parse Python IR annotation string -> PythonType
|
|
398
|
-
function parseAnnotationToPythonType(annotation: unknown, depth = 0): PythonType {
|
|
399
|
-
if (annotation === null || annotation === undefined) {
|
|
400
|
-
return unknownType();
|
|
401
|
-
}
|
|
402
|
-
if (depth > 100) {
|
|
403
|
-
return unknownType();
|
|
404
|
-
}
|
|
405
|
-
const raw = String(annotation).trim();
|
|
406
|
-
|
|
407
|
-
// Handle built-in class repr: <class 'int'>
|
|
408
|
-
const classMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
|
|
409
|
-
if (classMatch) {
|
|
410
|
-
const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
|
|
411
|
-
const name = (inner.split('.').pop() ?? '').toString();
|
|
412
|
-
return mapSimpleName(name);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// PEP 604 unions: int | str | None
|
|
416
|
-
if (raw.includes(' | ')) {
|
|
417
|
-
const parts = splitTopLevel(raw, '|');
|
|
418
|
-
const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
419
|
-
return { kind: 'union', types };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// typing.Union[...]
|
|
423
|
-
if (raw.startsWith('typing.Union[') || raw.startsWith('Union[')) {
|
|
424
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
425
|
-
const parts = splitTopLevel(inner, ',');
|
|
426
|
-
const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
427
|
-
return { kind: 'union', types };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Optional[T]
|
|
431
|
-
if (raw.startsWith('typing.Optional[') || raw.startsWith('Optional[')) {
|
|
432
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
433
|
-
const base = parseAnnotationToPythonType(inner, depth + 1);
|
|
434
|
-
return { kind: 'optional', type: base };
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Literal[...] -> literal values union
|
|
438
|
-
if (raw.startsWith('typing.Literal[') || raw.startsWith('Literal[')) {
|
|
439
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
440
|
-
const parts = splitTopLevel(inner, ',');
|
|
441
|
-
if (parts.length === 1) {
|
|
442
|
-
return mapLiteral(String(parts[0] ?? '').trim());
|
|
443
|
-
}
|
|
444
|
-
return { kind: 'union', types: parts.map(p => mapLiteral(String(p).trim())) } as PythonType;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// typing_extensions wrappers: ClassVar[T], Final[T], Required[T], NotRequired[T]
|
|
448
|
-
const extMatch = raw.match(
|
|
449
|
-
/^(typing\.|typing_extensions\.)?(ClassVar|Final|Required|NotRequired)\[(.*)\]$/
|
|
450
|
-
);
|
|
451
|
-
if (extMatch) {
|
|
452
|
-
const inner = extMatch[3] ?? '';
|
|
453
|
-
const base = parseAnnotationToPythonType(inner, depth + 1);
|
|
454
|
-
return base;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// LiteralString
|
|
458
|
-
if (raw === 'typing.LiteralString' || raw === 'LiteralString') {
|
|
459
|
-
return { kind: 'primitive', name: 'str' } as PythonType;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Callable[[...], R]
|
|
463
|
-
if (raw.startsWith('typing.Callable[') || raw.startsWith('Callable[')) {
|
|
464
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
465
|
-
const parts = splitTopLevel(inner, ',');
|
|
466
|
-
if (parts.length >= 2) {
|
|
467
|
-
const paramsPart = (parts[0] ?? '').trim();
|
|
468
|
-
const returnPart = parts.slice(1).join(',').trim();
|
|
469
|
-
const paramInner =
|
|
470
|
-
paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
|
|
471
|
-
const paramTypes = ((): PythonType[] => {
|
|
472
|
-
const trimmed = paramInner.trim();
|
|
473
|
-
if (trimmed === '...' || trimmed === 'Ellipsis') {
|
|
474
|
-
return [{ kind: 'custom', name: '...' } as PythonType];
|
|
475
|
-
}
|
|
476
|
-
return trimmed
|
|
477
|
-
? splitTopLevel(trimmed, ',').map(p => parseAnnotationToPythonType(p.trim(), depth + 1))
|
|
478
|
-
: [];
|
|
479
|
-
})();
|
|
480
|
-
const returnType = parseAnnotationToPythonType(returnPart, depth + 1);
|
|
481
|
-
return { kind: 'callable', parameters: paramTypes, returnType } as PythonType;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Mapping[K, V] / Dict[K, V] normalization
|
|
486
|
-
if (raw.startsWith('typing.Mapping[') || raw.startsWith('Mapping[')) {
|
|
487
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
488
|
-
const parts = splitTopLevel(inner, ',');
|
|
489
|
-
const itemTypes = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
490
|
-
return { kind: 'collection', name: 'dict', itemTypes } as PythonType;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Annotated[T, ...] -> annotated node with base and metadata
|
|
494
|
-
if (raw.startsWith('typing.Annotated[') || raw.startsWith('Annotated[')) {
|
|
495
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
496
|
-
const parts = splitTopLevel(inner, ',');
|
|
497
|
-
if (parts.length > 0) {
|
|
498
|
-
const base = parseAnnotationToPythonType((parts[0] ?? '').trim(), depth + 1);
|
|
499
|
-
const metaParts = parts.slice(1).map(p => String(p).trim());
|
|
500
|
-
return { kind: 'annotated', base, metadata: metaParts } as PythonType;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Collections: list[T], dict[K,V], tuple[...], set[T]
|
|
505
|
-
const coll = normalizeCollectionName(raw);
|
|
506
|
-
if (coll) {
|
|
507
|
-
const { name, inner } = coll;
|
|
508
|
-
const itemParts =
|
|
509
|
-
name === 'dict' ? splitTopLevel(inner ?? '', ',') : splitTopLevel(inner ?? '', ',');
|
|
510
|
-
const itemTypes = (inner ? itemParts : []).map(p =>
|
|
511
|
-
parseAnnotationToPythonType(p.trim(), depth + 1)
|
|
512
|
-
);
|
|
513
|
-
return { kind: 'collection', name, itemTypes };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// typing.Callable[...] unlikely for now - treat as custom
|
|
517
|
-
|
|
518
|
-
// Bare names like int, str, float, bool, bytes, None
|
|
519
|
-
return mapSimpleName(raw);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function mapSimpleName(name: string): PythonType {
|
|
523
|
-
const n = name.replace(/^typing\./, '').trim();
|
|
524
|
-
if (n === 'int' || n === 'float' || n === 'str' || n === 'bool' || n === 'bytes') {
|
|
525
|
-
// cast to the specific union type without using any
|
|
526
|
-
return { kind: 'primitive', name: n };
|
|
527
|
-
}
|
|
528
|
-
// Track unknown typing-ish names for diagnostics
|
|
529
|
-
if (
|
|
530
|
-
n === 'Any' ||
|
|
531
|
-
n === 'Never' ||
|
|
532
|
-
n === 'LiteralString' ||
|
|
533
|
-
n === 'ClassVar' ||
|
|
534
|
-
n === 'Final' ||
|
|
535
|
-
n === 'TypeAlias' ||
|
|
536
|
-
n === 'Required' ||
|
|
537
|
-
n === 'NotRequired'
|
|
538
|
-
) {
|
|
539
|
-
try {
|
|
540
|
-
recordUnknown(n);
|
|
541
|
-
} catch {}
|
|
542
|
-
}
|
|
543
|
-
if (n === 'None' || n.toLowerCase() === 'nonetype') {
|
|
544
|
-
return { kind: 'primitive', name: 'None' };
|
|
545
|
-
}
|
|
546
|
-
if (n === 'list' || n === 'List') {
|
|
547
|
-
return { kind: 'collection', name: 'list', itemTypes: [] };
|
|
548
|
-
}
|
|
549
|
-
if (n === 'dict' || n === 'Dict') {
|
|
550
|
-
return { kind: 'collection', name: 'dict', itemTypes: [] };
|
|
551
|
-
}
|
|
552
|
-
if (n === 'tuple' || n === 'Tuple') {
|
|
553
|
-
return { kind: 'collection', name: 'tuple', itemTypes: [] };
|
|
554
|
-
}
|
|
555
|
-
if (n === 'set' || n === 'Set') {
|
|
556
|
-
return { kind: 'collection', name: 'set', itemTypes: [] };
|
|
557
|
-
}
|
|
558
|
-
return { kind: 'custom', name: n };
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function normalizeCollectionName(
|
|
562
|
-
raw: string
|
|
563
|
-
): { name: 'list' | 'dict' | 'tuple' | 'set' | 'frozenset'; inner?: string } | null {
|
|
564
|
-
const m = raw.match(/^(typing\.)?(List|Dict|Tuple|Set|list|dict|tuple|set)\[(.*)\]$/);
|
|
565
|
-
if (!m) {
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
const nameRaw = String(m[2] ?? '');
|
|
569
|
-
const name =
|
|
570
|
-
nameRaw.toLowerCase() === 'list'
|
|
571
|
-
? 'list'
|
|
572
|
-
: nameRaw.toLowerCase() === 'dict'
|
|
573
|
-
? 'dict'
|
|
574
|
-
: nameRaw.toLowerCase() === 'tuple'
|
|
575
|
-
? 'tuple'
|
|
576
|
-
: 'set';
|
|
577
|
-
return { name, inner: String(m[3] ?? '') };
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function splitTopLevel(input: string, sep: '|' | ','): string[] {
|
|
581
|
-
const results: string[] = [];
|
|
582
|
-
let level = 0;
|
|
583
|
-
let cur = '';
|
|
584
|
-
let guard = 0;
|
|
585
|
-
for (let i = 0; i < input.length; i++) {
|
|
586
|
-
// Depth/length guard to avoid pathological recursion/loops
|
|
587
|
-
guard++;
|
|
588
|
-
if (guard > 20000) {
|
|
589
|
-
// Bail out safely; push remaining and break
|
|
590
|
-
if (cur.trim()) {
|
|
591
|
-
results.push(cur.trim());
|
|
592
|
-
}
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
const ch = input.charAt(i);
|
|
596
|
-
if (ch === '[' || ch === '(') {
|
|
597
|
-
level++;
|
|
598
|
-
}
|
|
599
|
-
if (ch === ']' || ch === ')') {
|
|
600
|
-
level = Math.max(0, level - 1);
|
|
601
|
-
}
|
|
602
|
-
if (level === 0 && input.slice(i, i + sep.length) === sep) {
|
|
603
|
-
results.push(cur.trim());
|
|
604
|
-
cur = '';
|
|
605
|
-
i += sep.length - 1;
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
cur += ch;
|
|
609
|
-
}
|
|
610
|
-
if (cur.trim()) {
|
|
611
|
-
results.push(cur.trim());
|
|
612
|
-
}
|
|
613
|
-
return results;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function mapLiteral(text: string): PythonType {
|
|
617
|
-
// strip quotes if present
|
|
618
|
-
const t = text.trim();
|
|
619
|
-
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
|
620
|
-
return { kind: 'literal', value: t.slice(1, -1) } as PythonType;
|
|
621
|
-
}
|
|
622
|
-
if (t === 'True' || t === 'False') {
|
|
623
|
-
return { kind: 'literal', value: t === 'True' } as PythonType;
|
|
624
|
-
}
|
|
625
|
-
if (t === 'None') {
|
|
626
|
-
return { kind: 'literal', value: null } as PythonType;
|
|
627
|
-
}
|
|
628
|
-
const num = Number(t);
|
|
629
|
-
if (!Number.isNaN(num)) {
|
|
630
|
-
return { kind: 'literal', value: num } as PythonType;
|
|
631
|
-
}
|
|
632
|
-
// fallback
|
|
633
|
-
return { kind: 'custom', name: t } as PythonType;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
609
|
/**
|
|
637
610
|
* Compute a stable cache key filename for a module and options
|
|
638
611
|
*/
|
|
@@ -650,6 +623,8 @@ async function computeCacheKey(
|
|
|
650
623
|
const keyObject = {
|
|
651
624
|
module: moduleName,
|
|
652
625
|
moduleVersion: moduleConfig?.version ?? null,
|
|
626
|
+
irVersion: TYWRAP_IR_VERSION,
|
|
627
|
+
pythonImportPath: options.pythonImportPath ?? [],
|
|
653
628
|
runtime: {
|
|
654
629
|
pythonPath: runtimePython,
|
|
655
630
|
virtualEnv: options.runtime?.node?.virtualEnv ?? null,
|
|
@@ -665,47 +640,7 @@ async function computeCacheKey(
|
|
|
665
640
|
},
|
|
666
641
|
typeHints: moduleConfig?.typeHints ?? 'strict',
|
|
667
642
|
} as const;
|
|
668
|
-
|
|
669
|
-
const digest = await hashUtils.sha256Hex(normalized);
|
|
670
|
-
return `ir_${moduleName}_${digest.slice(0, 16)}.json`;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Very lightweight .d.ts emitter derived from generated TS wrappers
|
|
675
|
-
* This is intentionally minimal and stable for our wrappers shape.
|
|
676
|
-
*/
|
|
677
|
-
function renderDts(generatedTs: string): string {
|
|
678
|
-
const header = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
|
|
679
|
-
const lines: string[] = [header];
|
|
680
|
-
// Extract function exports
|
|
681
|
-
const funcRegex = /export\s+async\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
|
|
682
|
-
let m: RegExpExecArray | null;
|
|
683
|
-
while ((m = funcRegex.exec(generatedTs)) !== null) {
|
|
684
|
-
const name = m[1];
|
|
685
|
-
const params = m[2];
|
|
686
|
-
const ret = m[3];
|
|
687
|
-
lines.push(`export function ${name}(${params}): Promise<${ret}>;`);
|
|
688
|
-
}
|
|
689
|
-
// Extract class exports and methods
|
|
690
|
-
const classRegex = /export\s+class\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
691
|
-
while ((m = classRegex.exec(generatedTs)) !== null) {
|
|
692
|
-
const className = String(m[1] ?? '');
|
|
693
|
-
const body = String(m[2] ?? '');
|
|
694
|
-
// Constructor is always variadic unknown[] in current generator
|
|
695
|
-
const methods: string[] = [];
|
|
696
|
-
const methodRegex = /\n\s+async\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
|
|
697
|
-
let mm: RegExpExecArray | null;
|
|
698
|
-
while ((mm = methodRegex.exec(body)) !== null) {
|
|
699
|
-
methods.push(` ${mm[1]}(${mm[2]}): Promise<${mm[3]}>;`);
|
|
700
|
-
}
|
|
701
|
-
lines.push(`export class ${className} {`);
|
|
702
|
-
lines.push(` constructor(...args: unknown[]);`);
|
|
703
|
-
if (methods.length > 0) {
|
|
704
|
-
lines.push(methods.join('\n'));
|
|
705
|
-
}
|
|
706
|
-
lines.push('}');
|
|
707
|
-
}
|
|
708
|
-
return `${lines.join('\n')}\n`;
|
|
643
|
+
return await computeIrCacheFilename(keyObject);
|
|
709
644
|
}
|
|
710
645
|
|
|
711
646
|
/**
|