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/dist/tywrap.js
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { CodeGenerator } from './core/generator.js';
|
|
5
5
|
import { TypeMapper } from './core/mapper.js';
|
|
6
|
+
import { parseAnnotationToPythonType } from './core/annotation-parser.js';
|
|
6
7
|
import { createConfig } from './config/index.js';
|
|
7
|
-
import { fsUtils, pathUtils, processUtils,
|
|
8
|
+
import { fsUtils, pathUtils, processUtils, isWindows } from './utils/runtime.js';
|
|
8
9
|
import { globalCache } from './utils/cache.js';
|
|
9
10
|
import { globalParallelProcessor } from './utils/parallel-processor.js';
|
|
10
11
|
import { resolvePythonExecutable } from './utils/python.js';
|
|
12
|
+
import { computeIrCacheFilename } from './utils/ir-cache.js';
|
|
13
|
+
const TYWRAP_IR_VERSION = '0.2.0';
|
|
11
14
|
// Collect unknown typing constructs encountered during annotation parsing (per-generate run)
|
|
12
15
|
let unknownTypeNamesCollector = new Map();
|
|
13
16
|
function recordUnknown(name) {
|
|
@@ -68,6 +71,7 @@ export async function generate(options, runOptions = {}) {
|
|
|
68
71
|
const written = [];
|
|
69
72
|
const outOfDate = [];
|
|
70
73
|
const warnings = [];
|
|
74
|
+
const failures = [];
|
|
71
75
|
const outputDir = resolvedOptions.output.dir;
|
|
72
76
|
const caching = resolvedOptions.performance.caching;
|
|
73
77
|
const cacheDir = '.tywrap/cache';
|
|
@@ -88,13 +92,14 @@ export async function generate(options, runOptions = {}) {
|
|
|
88
92
|
const modules = resolvedOptions.pythonModules ?? {};
|
|
89
93
|
for (const entry of Object.entries(modules)) {
|
|
90
94
|
const moduleKey = entry[0];
|
|
95
|
+
const moduleConfig = entry[1];
|
|
91
96
|
// reset collector for each module
|
|
92
97
|
unknownTypeNamesCollector = new Map();
|
|
93
98
|
// Prefer Python IR extractor over TS analyzer with optional cache
|
|
94
|
-
const cacheKey = await computeCacheKey(moduleKey,
|
|
99
|
+
const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
|
|
95
100
|
let ir = null;
|
|
96
101
|
let irError;
|
|
97
|
-
if (caching && fsUtils.isAvailable()) {
|
|
102
|
+
if (caching && fsUtils.isAvailable() && !checkMode) {
|
|
98
103
|
try {
|
|
99
104
|
const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
|
|
100
105
|
ir = JSON.parse(cached);
|
|
@@ -104,7 +109,10 @@ export async function generate(options, runOptions = {}) {
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
if (!ir) {
|
|
107
|
-
const fetchResult = await fetchPythonIr(moduleKey, pythonPath
|
|
112
|
+
const fetchResult = await fetchPythonIr(moduleKey, pythonPath, {
|
|
113
|
+
timeoutMs: resolvedOptions.runtime?.node?.timeout,
|
|
114
|
+
pythonImportPath: resolvedOptions.pythonImportPath,
|
|
115
|
+
});
|
|
108
116
|
ir = fetchResult.ir;
|
|
109
117
|
irError = fetchResult.error;
|
|
110
118
|
if (ir && caching && fsUtils.isAvailable() && !checkMode) {
|
|
@@ -115,10 +123,75 @@ export async function generate(options, runOptions = {}) {
|
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
125
|
if (!ir) {
|
|
118
|
-
|
|
126
|
+
const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
|
|
127
|
+
warnings.push(message);
|
|
128
|
+
failures.push({
|
|
129
|
+
module: moduleKey,
|
|
130
|
+
code: 'ir-unavailable',
|
|
131
|
+
message,
|
|
132
|
+
});
|
|
119
133
|
continue;
|
|
120
134
|
}
|
|
121
135
|
const moduleModel = transformIrToTsModel(ir);
|
|
136
|
+
// Apply module-level export filtering (functions/classes + excludes).
|
|
137
|
+
{
|
|
138
|
+
const builtInDefaultExcludes = new Set([
|
|
139
|
+
'dataclass',
|
|
140
|
+
'property',
|
|
141
|
+
'staticmethod',
|
|
142
|
+
'classmethod',
|
|
143
|
+
'abstractmethod',
|
|
144
|
+
'cached_property',
|
|
145
|
+
]);
|
|
146
|
+
const excludeExact = new Set((moduleConfig.exclude ?? []).map(String));
|
|
147
|
+
const excludeRegexes = [];
|
|
148
|
+
for (const pattern of moduleConfig.excludePatterns ?? []) {
|
|
149
|
+
try {
|
|
150
|
+
excludeRegexes.push(new RegExp(String(pattern)));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
+
warnings.push(`Module ${moduleKey}: invalid excludePatterns regex "${String(pattern)}": ${message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const shouldExclude = (name, applyBuiltInDefaults) => {
|
|
158
|
+
if (excludeExact.has(name)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (excludeRegexes.some(r => r.test(name))) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (applyBuiltInDefaults && builtInDefaultExcludes.has(name)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
if (Array.isArray(moduleConfig.functions)) {
|
|
170
|
+
const allow = new Set(moduleConfig.functions.map(String));
|
|
171
|
+
for (const requested of allow) {
|
|
172
|
+
if (!moduleModel.functions.some(f => f.name === requested)) {
|
|
173
|
+
warnings.push(`Module ${moduleKey}: configured function "${requested}" not found in IR`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
moduleModel.functions = moduleModel.functions.filter(f => allow.has(f.name) && !shouldExclude(f.name, false));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
moduleModel.functions = moduleModel.functions.filter(f => !shouldExclude(f.name, true));
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(moduleConfig.classes)) {
|
|
182
|
+
const allow = new Set(moduleConfig.classes.map(String));
|
|
183
|
+
for (const requested of allow) {
|
|
184
|
+
if (!moduleModel.classes.some(c => c.name === requested)) {
|
|
185
|
+
warnings.push(`Module ${moduleKey}: configured class "${requested}" not found in IR`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
moduleModel.classes = moduleModel.classes.filter(c => allow.has(c.name) && !shouldExclude(c.name, false));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
moduleModel.classes = moduleModel.classes.filter(c => !shouldExclude(c.name, true));
|
|
192
|
+
}
|
|
193
|
+
moduleModel.typeAliases = (moduleModel.typeAliases ?? []).filter(alias => !shouldExclude(alias.name, false));
|
|
194
|
+
}
|
|
122
195
|
// Generate module code
|
|
123
196
|
const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
|
|
124
197
|
const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
|
|
@@ -130,7 +203,7 @@ export async function generate(options, runOptions = {}) {
|
|
|
130
203
|
if (resolvedOptions.output?.declaration) {
|
|
131
204
|
filesToEmit.push({
|
|
132
205
|
path: pathUtils.join(outputDir, `${baseName}.generated.d.ts`),
|
|
133
|
-
content:
|
|
206
|
+
content: gen.declaration,
|
|
134
207
|
});
|
|
135
208
|
}
|
|
136
209
|
// Optional source map emission (placeholder mapping for now)
|
|
@@ -183,25 +256,28 @@ export async function generate(options, runOptions = {}) {
|
|
|
183
256
|
}
|
|
184
257
|
}
|
|
185
258
|
if (checkMode) {
|
|
186
|
-
return { written: [], warnings, outOfDate };
|
|
259
|
+
return { written: [], warnings, failures, outOfDate };
|
|
187
260
|
}
|
|
188
|
-
return { written, warnings };
|
|
261
|
+
return { written, warnings, failures };
|
|
189
262
|
}
|
|
190
263
|
/**
|
|
191
264
|
* Invoke the Python IR CLI to get JSON IR for a module.
|
|
192
265
|
*/
|
|
193
|
-
async function fetchPythonIr(moduleName, pythonPath) {
|
|
266
|
+
async function fetchPythonIr(moduleName, pythonPath, options = {}) {
|
|
194
267
|
if (!processUtils.isAvailable()) {
|
|
195
268
|
return { ir: null, error: 'Subprocess operations not available in this runtime' };
|
|
196
269
|
}
|
|
270
|
+
const delimiter = isWindows() ? ';' : ':';
|
|
271
|
+
const extraPaths = (options.pythonImportPath ?? []).filter(Boolean);
|
|
272
|
+
const existingPyPath = typeof process !== 'undefined' && typeof process.env === 'object' && process.env
|
|
273
|
+
? process.env.PYTHONPATH
|
|
274
|
+
: undefined;
|
|
275
|
+
const mergedPyPath = [...extraPaths, ...(existingPyPath ? [existingPyPath] : [])]
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.join(delimiter);
|
|
278
|
+
const env = mergedPyPath ? { PYTHONPATH: mergedPyPath } : undefined;
|
|
197
279
|
try {
|
|
198
|
-
const result = await processUtils.exec(pythonPath, [
|
|
199
|
-
'-m',
|
|
200
|
-
'tywrap_ir',
|
|
201
|
-
'--module',
|
|
202
|
-
moduleName,
|
|
203
|
-
'--no-pretty',
|
|
204
|
-
]);
|
|
280
|
+
const result = await processUtils.exec(pythonPath, ['-m', 'tywrap_ir', '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'], { timeoutMs: options.timeoutMs, env });
|
|
205
281
|
if (result.code === 0) {
|
|
206
282
|
try {
|
|
207
283
|
return { ir: JSON.parse(result.stdout) };
|
|
@@ -213,18 +289,34 @@ async function fetchPythonIr(moduleName, pythonPath) {
|
|
|
213
289
|
};
|
|
214
290
|
}
|
|
215
291
|
}
|
|
216
|
-
|
|
292
|
+
const stderrText = result.stderr.trim();
|
|
293
|
+
const isTywrapIrMissing = stderrText.includes('No module named') && stderrText.includes('tywrap_ir');
|
|
294
|
+
if (!isTywrapIrMissing) {
|
|
295
|
+
return { ir: null, error: `tywrap_ir failed. stderr: ${stderrText || 'empty'}` };
|
|
296
|
+
}
|
|
297
|
+
// Fallback to invoking local __main__.py (useful when running from the repo).
|
|
217
298
|
const localMain = pathUtils.join(process.cwd(), 'tywrap_ir', 'tywrap_ir', '__main__.py');
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
299
|
+
let localMainExists = false;
|
|
300
|
+
if (fsUtils.isAvailable()) {
|
|
301
|
+
try {
|
|
302
|
+
await fsUtils.readFile(localMain);
|
|
303
|
+
localMainExists = true;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
localMainExists = false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!localMainExists) {
|
|
310
|
+
return {
|
|
311
|
+
ir: null,
|
|
312
|
+
error: `tywrap_ir not found on PYTHONPATH. stderr: ${stderrText || 'empty'}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const fallback = await processUtils.exec(pythonPath, [localMain, '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'], { timeoutMs: options.timeoutMs, env });
|
|
224
316
|
if (fallback.code !== 0) {
|
|
225
317
|
return {
|
|
226
318
|
ir: null,
|
|
227
|
-
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() ||
|
|
319
|
+
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || stderrText || 'empty'}`,
|
|
228
320
|
};
|
|
229
321
|
}
|
|
230
322
|
try {
|
|
@@ -244,70 +336,134 @@ async function fetchPythonIr(moduleName, pythonPath) {
|
|
|
244
336
|
/**
|
|
245
337
|
* Convert JSON IR from Python into the internal TypeScript model used by the generator.
|
|
246
338
|
*/
|
|
339
|
+
function collectModuleTypeVarNames(obj) {
|
|
340
|
+
const names = new Set();
|
|
341
|
+
const constants = Array.isArray(obj.constants) ? obj.constants : [];
|
|
342
|
+
for (const constant of constants) {
|
|
343
|
+
const entry = constant;
|
|
344
|
+
const name = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
345
|
+
if (!name) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const valueRepr = typeof entry.value_repr === 'string' ? entry.value_repr.trim() : '';
|
|
349
|
+
if (/^~?[A-Za-z_][A-Za-z0-9_]*$/.test(valueRepr) && valueRepr.replace(/^~/, '') === name) {
|
|
350
|
+
names.add(name);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const annotation = typeof entry.annotation === 'string' ? entry.annotation.trim() : '';
|
|
354
|
+
if (/(^|\.)(TypeVar|ParamSpec|TypeVarTuple)(\[|\(|$)/.test(annotation)) {
|
|
355
|
+
names.add(name);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return names;
|
|
359
|
+
}
|
|
247
360
|
function transformIrToTsModel(ir) {
|
|
248
361
|
const obj = typeof ir === 'object' && ir !== null ? ir : {};
|
|
249
362
|
const functions = obj.functions ?? [];
|
|
250
363
|
const classes = obj.classes ?? [];
|
|
251
|
-
const
|
|
364
|
+
const aliases = obj.type_aliases ?? [];
|
|
365
|
+
const moduleTypeVarNames = collectModuleTypeVarNames(obj);
|
|
366
|
+
const parseType = (annotation, typeParameters = []) => parseAnnotationToPythonType(annotation, {
|
|
367
|
+
onUnknownTypeName: recordUnknown,
|
|
368
|
+
knownTypeVarNames: moduleTypeVarNames,
|
|
369
|
+
typeParameters,
|
|
370
|
+
});
|
|
371
|
+
const mapTypeParameters = (value) => Array.isArray(value.type_params)
|
|
372
|
+
? value.type_params.map(v => {
|
|
373
|
+
const param = (v ?? {});
|
|
374
|
+
return {
|
|
375
|
+
name: String(param.name ?? ''),
|
|
376
|
+
kind: String(param.kind ?? 'typevar'),
|
|
377
|
+
bound: param.bound ? parseType(param.bound) : undefined,
|
|
378
|
+
constraints: Array.isArray(param.constraints)
|
|
379
|
+
? param.constraints.map(item => parseType(item))
|
|
380
|
+
: undefined,
|
|
381
|
+
variance: param.variance === 'covariant' ||
|
|
382
|
+
param.variance === 'contravariant' ||
|
|
383
|
+
param.variance === 'invariant'
|
|
384
|
+
? param.variance
|
|
385
|
+
: undefined,
|
|
386
|
+
};
|
|
387
|
+
})
|
|
388
|
+
: [];
|
|
389
|
+
const mapParam = (p, typeParameters = []) => ({
|
|
252
390
|
name: String(p.name ?? ''),
|
|
253
|
-
type:
|
|
391
|
+
type: parseType(p.annotation, typeParameters),
|
|
254
392
|
optional: Boolean(p.default),
|
|
255
393
|
varArgs: p.kind === 'VAR_POSITIONAL',
|
|
256
394
|
kwArgs: p.kind === 'VAR_KEYWORD',
|
|
395
|
+
positionalOnly: p.kind === 'POSITIONAL_ONLY',
|
|
396
|
+
keywordOnly: p.kind === 'KEYWORD_ONLY',
|
|
257
397
|
});
|
|
258
|
-
const mapFunc = (f) =>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
398
|
+
const mapFunc = (f, inheritedTypeParameters = []) => {
|
|
399
|
+
const localTypeParameters = mapTypeParameters(f);
|
|
400
|
+
const annotationTypeParameters = [...inheritedTypeParameters, ...localTypeParameters];
|
|
401
|
+
return {
|
|
402
|
+
name: String(f.name ?? ''),
|
|
403
|
+
signature: {
|
|
404
|
+
parameters: Array.isArray(f.parameters)
|
|
405
|
+
? f.parameters.map(v => mapParam((v ?? {}), annotationTypeParameters))
|
|
406
|
+
: [],
|
|
407
|
+
returnType: parseType(f.returns, annotationTypeParameters),
|
|
408
|
+
isAsync: Boolean(f.is_async),
|
|
409
|
+
isGenerator: Boolean(f.is_generator),
|
|
410
|
+
},
|
|
411
|
+
docstring: f.docstring ?? undefined,
|
|
412
|
+
decorators: [],
|
|
265
413
|
isAsync: Boolean(f.is_async),
|
|
266
414
|
isGenerator: Boolean(f.is_generator),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
: [],
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
415
|
+
typeParameters: [...localTypeParameters],
|
|
416
|
+
returnType: parseType(f.returns, annotationTypeParameters),
|
|
417
|
+
parameters: Array.isArray(f.parameters)
|
|
418
|
+
? f.parameters.map(v => mapParam((v ?? {}), annotationTypeParameters))
|
|
419
|
+
: [],
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
const mapClass = (c) => {
|
|
423
|
+
const classTypeParameters = mapTypeParameters(c);
|
|
424
|
+
return {
|
|
425
|
+
name: String(c.name ?? ''),
|
|
426
|
+
bases: Array.isArray(c.bases) ? c.bases : [],
|
|
427
|
+
methods: Array.isArray(c.methods)
|
|
428
|
+
? c.methods.map(v => mapFunc((v ?? {}), classTypeParameters))
|
|
429
|
+
: [],
|
|
430
|
+
properties: Array.isArray(c.fields)
|
|
431
|
+
? c.fields.map(v => {
|
|
432
|
+
const p = (v ?? {});
|
|
433
|
+
return {
|
|
434
|
+
name: String(p.name ?? ''),
|
|
435
|
+
type: parseType(p.annotation, classTypeParameters),
|
|
436
|
+
readonly: false,
|
|
437
|
+
setter: false,
|
|
438
|
+
getter: true,
|
|
439
|
+
optional: Boolean(p.default),
|
|
440
|
+
};
|
|
441
|
+
})
|
|
442
|
+
: [],
|
|
443
|
+
docstring: c.docstring ?? undefined,
|
|
444
|
+
decorators: c.typed_dict ? ['__typed_dict__'] : [],
|
|
445
|
+
kind: c.typed_dict
|
|
446
|
+
? 'typed_dict'
|
|
447
|
+
: c.is_protocol
|
|
448
|
+
? 'protocol'
|
|
449
|
+
: c.is_namedtuple
|
|
450
|
+
? 'namedtuple'
|
|
451
|
+
: c.is_dataclass
|
|
452
|
+
? 'dataclass'
|
|
453
|
+
: c.is_pydantic
|
|
454
|
+
? 'pydantic'
|
|
455
|
+
: 'class',
|
|
456
|
+
typeParameters: classTypeParameters,
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
const mapTypeAlias = (alias) => {
|
|
460
|
+
const typeParameters = mapTypeParameters(alias);
|
|
461
|
+
return {
|
|
462
|
+
name: String(alias.name ?? ''),
|
|
463
|
+
type: parseType(alias.definition, typeParameters),
|
|
464
|
+
typeParameters,
|
|
465
|
+
};
|
|
466
|
+
};
|
|
311
467
|
const moduleModel = {
|
|
312
468
|
name: obj.module ?? 'module',
|
|
313
469
|
path: undefined,
|
|
@@ -316,225 +472,12 @@ function transformIrToTsModel(ir) {
|
|
|
316
472
|
: undefined,
|
|
317
473
|
functions: functions.map(v => mapFunc((v ?? {}))),
|
|
318
474
|
classes: classes.map(v => mapClass((v ?? {}))),
|
|
475
|
+
typeAliases: aliases.map(v => mapTypeAlias((v ?? {}))),
|
|
319
476
|
imports: [],
|
|
320
477
|
exports: [],
|
|
321
478
|
};
|
|
322
479
|
return moduleModel;
|
|
323
480
|
}
|
|
324
|
-
function unknownType() {
|
|
325
|
-
return { kind: 'custom', name: 'Any', module: 'typing' };
|
|
326
|
-
}
|
|
327
|
-
// Parse Python IR annotation string -> PythonType
|
|
328
|
-
function parseAnnotationToPythonType(annotation, depth = 0) {
|
|
329
|
-
if (annotation === null || annotation === undefined) {
|
|
330
|
-
return unknownType();
|
|
331
|
-
}
|
|
332
|
-
if (depth > 100) {
|
|
333
|
-
return unknownType();
|
|
334
|
-
}
|
|
335
|
-
const raw = String(annotation).trim();
|
|
336
|
-
// Handle built-in class repr: <class 'int'>
|
|
337
|
-
const classMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
|
|
338
|
-
if (classMatch) {
|
|
339
|
-
const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
|
|
340
|
-
const name = (inner.split('.').pop() ?? '').toString();
|
|
341
|
-
return mapSimpleName(name);
|
|
342
|
-
}
|
|
343
|
-
// PEP 604 unions: int | str | None
|
|
344
|
-
if (raw.includes(' | ')) {
|
|
345
|
-
const parts = splitTopLevel(raw, '|');
|
|
346
|
-
const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
347
|
-
return { kind: 'union', types };
|
|
348
|
-
}
|
|
349
|
-
// typing.Union[...]
|
|
350
|
-
if (raw.startsWith('typing.Union[') || raw.startsWith('Union[')) {
|
|
351
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
352
|
-
const parts = splitTopLevel(inner, ',');
|
|
353
|
-
const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
354
|
-
return { kind: 'union', types };
|
|
355
|
-
}
|
|
356
|
-
// Optional[T]
|
|
357
|
-
if (raw.startsWith('typing.Optional[') || raw.startsWith('Optional[')) {
|
|
358
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
359
|
-
const base = parseAnnotationToPythonType(inner, depth + 1);
|
|
360
|
-
return { kind: 'optional', type: base };
|
|
361
|
-
}
|
|
362
|
-
// Literal[...] -> literal values union
|
|
363
|
-
if (raw.startsWith('typing.Literal[') || raw.startsWith('Literal[')) {
|
|
364
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
365
|
-
const parts = splitTopLevel(inner, ',');
|
|
366
|
-
if (parts.length === 1) {
|
|
367
|
-
return mapLiteral(String(parts[0] ?? '').trim());
|
|
368
|
-
}
|
|
369
|
-
return { kind: 'union', types: parts.map(p => mapLiteral(String(p).trim())) };
|
|
370
|
-
}
|
|
371
|
-
// typing_extensions wrappers: ClassVar[T], Final[T], Required[T], NotRequired[T]
|
|
372
|
-
const extMatch = raw.match(/^(typing\.|typing_extensions\.)?(ClassVar|Final|Required|NotRequired)\[(.*)\]$/);
|
|
373
|
-
if (extMatch) {
|
|
374
|
-
const inner = extMatch[3] ?? '';
|
|
375
|
-
const base = parseAnnotationToPythonType(inner, depth + 1);
|
|
376
|
-
return base;
|
|
377
|
-
}
|
|
378
|
-
// LiteralString
|
|
379
|
-
if (raw === 'typing.LiteralString' || raw === 'LiteralString') {
|
|
380
|
-
return { kind: 'primitive', name: 'str' };
|
|
381
|
-
}
|
|
382
|
-
// Callable[[...], R]
|
|
383
|
-
if (raw.startsWith('typing.Callable[') || raw.startsWith('Callable[')) {
|
|
384
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
385
|
-
const parts = splitTopLevel(inner, ',');
|
|
386
|
-
if (parts.length >= 2) {
|
|
387
|
-
const paramsPart = (parts[0] ?? '').trim();
|
|
388
|
-
const returnPart = parts.slice(1).join(',').trim();
|
|
389
|
-
const paramInner = paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
|
|
390
|
-
const paramTypes = (() => {
|
|
391
|
-
const trimmed = paramInner.trim();
|
|
392
|
-
if (trimmed === '...' || trimmed === 'Ellipsis') {
|
|
393
|
-
return [{ kind: 'custom', name: '...' }];
|
|
394
|
-
}
|
|
395
|
-
return trimmed
|
|
396
|
-
? splitTopLevel(trimmed, ',').map(p => parseAnnotationToPythonType(p.trim(), depth + 1))
|
|
397
|
-
: [];
|
|
398
|
-
})();
|
|
399
|
-
const returnType = parseAnnotationToPythonType(returnPart, depth + 1);
|
|
400
|
-
return { kind: 'callable', parameters: paramTypes, returnType };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Mapping[K, V] / Dict[K, V] normalization
|
|
404
|
-
if (raw.startsWith('typing.Mapping[') || raw.startsWith('Mapping[')) {
|
|
405
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
406
|
-
const parts = splitTopLevel(inner, ',');
|
|
407
|
-
const itemTypes = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
408
|
-
return { kind: 'collection', name: 'dict', itemTypes };
|
|
409
|
-
}
|
|
410
|
-
// Annotated[T, ...] -> annotated node with base and metadata
|
|
411
|
-
if (raw.startsWith('typing.Annotated[') || raw.startsWith('Annotated[')) {
|
|
412
|
-
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
413
|
-
const parts = splitTopLevel(inner, ',');
|
|
414
|
-
if (parts.length > 0) {
|
|
415
|
-
const base = parseAnnotationToPythonType((parts[0] ?? '').trim(), depth + 1);
|
|
416
|
-
const metaParts = parts.slice(1).map(p => String(p).trim());
|
|
417
|
-
return { kind: 'annotated', base, metadata: metaParts };
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// Collections: list[T], dict[K,V], tuple[...], set[T]
|
|
421
|
-
const coll = normalizeCollectionName(raw);
|
|
422
|
-
if (coll) {
|
|
423
|
-
const { name, inner } = coll;
|
|
424
|
-
const itemParts = name === 'dict' ? splitTopLevel(inner ?? '', ',') : splitTopLevel(inner ?? '', ',');
|
|
425
|
-
const itemTypes = (inner ? itemParts : []).map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
426
|
-
return { kind: 'collection', name, itemTypes };
|
|
427
|
-
}
|
|
428
|
-
// typing.Callable[...] unlikely for now - treat as custom
|
|
429
|
-
// Bare names like int, str, float, bool, bytes, None
|
|
430
|
-
return mapSimpleName(raw);
|
|
431
|
-
}
|
|
432
|
-
function mapSimpleName(name) {
|
|
433
|
-
const n = name.replace(/^typing\./, '').trim();
|
|
434
|
-
if (n === 'int' || n === 'float' || n === 'str' || n === 'bool' || n === 'bytes') {
|
|
435
|
-
// cast to the specific union type without using any
|
|
436
|
-
return { kind: 'primitive', name: n };
|
|
437
|
-
}
|
|
438
|
-
// Track unknown typing-ish names for diagnostics
|
|
439
|
-
if (n === 'Any' ||
|
|
440
|
-
n === 'Never' ||
|
|
441
|
-
n === 'LiteralString' ||
|
|
442
|
-
n === 'ClassVar' ||
|
|
443
|
-
n === 'Final' ||
|
|
444
|
-
n === 'TypeAlias' ||
|
|
445
|
-
n === 'Required' ||
|
|
446
|
-
n === 'NotRequired') {
|
|
447
|
-
try {
|
|
448
|
-
recordUnknown(n);
|
|
449
|
-
}
|
|
450
|
-
catch { }
|
|
451
|
-
}
|
|
452
|
-
if (n === 'None' || n.toLowerCase() === 'nonetype') {
|
|
453
|
-
return { kind: 'primitive', name: 'None' };
|
|
454
|
-
}
|
|
455
|
-
if (n === 'list' || n === 'List') {
|
|
456
|
-
return { kind: 'collection', name: 'list', itemTypes: [] };
|
|
457
|
-
}
|
|
458
|
-
if (n === 'dict' || n === 'Dict') {
|
|
459
|
-
return { kind: 'collection', name: 'dict', itemTypes: [] };
|
|
460
|
-
}
|
|
461
|
-
if (n === 'tuple' || n === 'Tuple') {
|
|
462
|
-
return { kind: 'collection', name: 'tuple', itemTypes: [] };
|
|
463
|
-
}
|
|
464
|
-
if (n === 'set' || n === 'Set') {
|
|
465
|
-
return { kind: 'collection', name: 'set', itemTypes: [] };
|
|
466
|
-
}
|
|
467
|
-
return { kind: 'custom', name: n };
|
|
468
|
-
}
|
|
469
|
-
function normalizeCollectionName(raw) {
|
|
470
|
-
const m = raw.match(/^(typing\.)?(List|Dict|Tuple|Set|list|dict|tuple|set)\[(.*)\]$/);
|
|
471
|
-
if (!m) {
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
const nameRaw = String(m[2] ?? '');
|
|
475
|
-
const name = nameRaw.toLowerCase() === 'list'
|
|
476
|
-
? 'list'
|
|
477
|
-
: nameRaw.toLowerCase() === 'dict'
|
|
478
|
-
? 'dict'
|
|
479
|
-
: nameRaw.toLowerCase() === 'tuple'
|
|
480
|
-
? 'tuple'
|
|
481
|
-
: 'set';
|
|
482
|
-
return { name, inner: String(m[3] ?? '') };
|
|
483
|
-
}
|
|
484
|
-
function splitTopLevel(input, sep) {
|
|
485
|
-
const results = [];
|
|
486
|
-
let level = 0;
|
|
487
|
-
let cur = '';
|
|
488
|
-
let guard = 0;
|
|
489
|
-
for (let i = 0; i < input.length; i++) {
|
|
490
|
-
// Depth/length guard to avoid pathological recursion/loops
|
|
491
|
-
guard++;
|
|
492
|
-
if (guard > 20000) {
|
|
493
|
-
// Bail out safely; push remaining and break
|
|
494
|
-
if (cur.trim()) {
|
|
495
|
-
results.push(cur.trim());
|
|
496
|
-
}
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
const ch = input.charAt(i);
|
|
500
|
-
if (ch === '[' || ch === '(') {
|
|
501
|
-
level++;
|
|
502
|
-
}
|
|
503
|
-
if (ch === ']' || ch === ')') {
|
|
504
|
-
level = Math.max(0, level - 1);
|
|
505
|
-
}
|
|
506
|
-
if (level === 0 && input.slice(i, i + sep.length) === sep) {
|
|
507
|
-
results.push(cur.trim());
|
|
508
|
-
cur = '';
|
|
509
|
-
i += sep.length - 1;
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
cur += ch;
|
|
513
|
-
}
|
|
514
|
-
if (cur.trim()) {
|
|
515
|
-
results.push(cur.trim());
|
|
516
|
-
}
|
|
517
|
-
return results;
|
|
518
|
-
}
|
|
519
|
-
function mapLiteral(text) {
|
|
520
|
-
// strip quotes if present
|
|
521
|
-
const t = text.trim();
|
|
522
|
-
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
|
523
|
-
return { kind: 'literal', value: t.slice(1, -1) };
|
|
524
|
-
}
|
|
525
|
-
if (t === 'True' || t === 'False') {
|
|
526
|
-
return { kind: 'literal', value: t === 'True' };
|
|
527
|
-
}
|
|
528
|
-
if (t === 'None') {
|
|
529
|
-
return { kind: 'literal', value: null };
|
|
530
|
-
}
|
|
531
|
-
const num = Number(t);
|
|
532
|
-
if (!Number.isNaN(num)) {
|
|
533
|
-
return { kind: 'literal', value: num };
|
|
534
|
-
}
|
|
535
|
-
// fallback
|
|
536
|
-
return { kind: 'custom', name: t };
|
|
537
|
-
}
|
|
538
481
|
/**
|
|
539
482
|
* Compute a stable cache key filename for a module and options
|
|
540
483
|
*/
|
|
@@ -549,6 +492,8 @@ async function computeCacheKey(moduleName, options) {
|
|
|
549
492
|
const keyObject = {
|
|
550
493
|
module: moduleName,
|
|
551
494
|
moduleVersion: moduleConfig?.version ?? null,
|
|
495
|
+
irVersion: TYWRAP_IR_VERSION,
|
|
496
|
+
pythonImportPath: options.pythonImportPath ?? [],
|
|
552
497
|
runtime: {
|
|
553
498
|
pythonPath: runtimePython,
|
|
554
499
|
virtualEnv: options.runtime?.node?.virtualEnv ?? null,
|
|
@@ -564,46 +509,7 @@ async function computeCacheKey(moduleName, options) {
|
|
|
564
509
|
},
|
|
565
510
|
typeHints: moduleConfig?.typeHints ?? 'strict',
|
|
566
511
|
};
|
|
567
|
-
|
|
568
|
-
const digest = await hashUtils.sha256Hex(normalized);
|
|
569
|
-
return `ir_${moduleName}_${digest.slice(0, 16)}.json`;
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Very lightweight .d.ts emitter derived from generated TS wrappers
|
|
573
|
-
* This is intentionally minimal and stable for our wrappers shape.
|
|
574
|
-
*/
|
|
575
|
-
function renderDts(generatedTs) {
|
|
576
|
-
const header = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
|
|
577
|
-
const lines = [header];
|
|
578
|
-
// Extract function exports
|
|
579
|
-
const funcRegex = /export\s+async\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
|
|
580
|
-
let m;
|
|
581
|
-
while ((m = funcRegex.exec(generatedTs)) !== null) {
|
|
582
|
-
const name = m[1];
|
|
583
|
-
const params = m[2];
|
|
584
|
-
const ret = m[3];
|
|
585
|
-
lines.push(`export function ${name}(${params}): Promise<${ret}>;`);
|
|
586
|
-
}
|
|
587
|
-
// Extract class exports and methods
|
|
588
|
-
const classRegex = /export\s+class\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
589
|
-
while ((m = classRegex.exec(generatedTs)) !== null) {
|
|
590
|
-
const className = String(m[1] ?? '');
|
|
591
|
-
const body = String(m[2] ?? '');
|
|
592
|
-
// Constructor is always variadic unknown[] in current generator
|
|
593
|
-
const methods = [];
|
|
594
|
-
const methodRegex = /\n\s+async\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
|
|
595
|
-
let mm;
|
|
596
|
-
while ((mm = methodRegex.exec(body)) !== null) {
|
|
597
|
-
methods.push(` ${mm[1]}(${mm[2]}): Promise<${mm[3]}>;`);
|
|
598
|
-
}
|
|
599
|
-
lines.push(`export class ${className} {`);
|
|
600
|
-
lines.push(` constructor(...args: unknown[]);`);
|
|
601
|
-
if (methods.length > 0) {
|
|
602
|
-
lines.push(methods.join('\n'));
|
|
603
|
-
}
|
|
604
|
-
lines.push('}');
|
|
605
|
-
}
|
|
606
|
-
return `${lines.join('\n')}\n`;
|
|
512
|
+
return await computeIrCacheFilename(keyObject);
|
|
607
513
|
}
|
|
608
514
|
/**
|
|
609
515
|
* Minimal source map placeholder (stable, empty mappings)
|