tywrap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +263 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +41 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +234 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/analyzer.d.ts +63 -0
- package/dist/core/analyzer.d.ts.map +1 -0
- package/dist/core/analyzer.js +676 -0
- package/dist/core/analyzer.js.map +1 -0
- package/dist/core/discovery.d.ts +85 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +344 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/generator.d.ts +37 -0
- package/dist/core/generator.d.ts.map +1 -0
- package/dist/core/generator.js +368 -0
- package/dist/core/generator.js.map +1 -0
- package/dist/core/mapper.d.ts +40 -0
- package/dist/core/mapper.d.ts.map +1 -0
- package/dist/core/mapper.js +401 -0
- package/dist/core/mapper.js.map +1 -0
- package/dist/core/validation.d.ts +79 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +486 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/base.d.ts +12 -0
- package/dist/runtime/base.d.ts.map +1 -0
- package/dist/runtime/base.js +6 -0
- package/dist/runtime/base.js.map +1 -0
- package/dist/runtime/errors.d.ts +17 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +21 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/http.d.ts +22 -0
- package/dist/runtime/http.d.ts.map +1 -0
- package/dist/runtime/http.js +79 -0
- package/dist/runtime/http.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node.d.ts +48 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +328 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/optimized-node.d.ts +131 -0
- package/dist/runtime/optimized-node.d.ts.map +1 -0
- package/dist/runtime/optimized-node.js +622 -0
- package/dist/runtime/optimized-node.js.map +1 -0
- package/dist/runtime/protocol.d.ts +3 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +3 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +27 -0
- package/dist/runtime/pyodide.d.ts.map +1 -0
- package/dist/runtime/pyodide.js +191 -0
- package/dist/runtime/pyodide.js.map +1 -0
- package/dist/tools/python_suite.js +173 -0
- package/dist/tools/python_suite.js.map +1 -0
- package/dist/types/index.d.ts +299 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/tywrap.d.ts +29 -0
- package/dist/tywrap.d.ts.map +1 -0
- package/dist/tywrap.js +589 -0
- package/dist/tywrap.js.map +1 -0
- package/dist/utils/bundle-optimizer.d.ts +182 -0
- package/dist/utils/bundle-optimizer.d.ts.map +1 -0
- package/dist/utils/bundle-optimizer.js +680 -0
- package/dist/utils/bundle-optimizer.js.map +1 -0
- package/dist/utils/cache.d.ts +149 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +492 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/codec.d.ts +94 -0
- package/dist/utils/codec.d.ts.map +1 -0
- package/dist/utils/codec.js +150 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/logger.d.ts +42 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +140 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/memory-profiler.d.ts +123 -0
- package/dist/utils/memory-profiler.d.ts.map +1 -0
- package/dist/utils/memory-profiler.js +426 -0
- package/dist/utils/memory-profiler.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +146 -0
- package/dist/utils/parallel-processor.d.ts.map +1 -0
- package/dist/utils/parallel-processor.js +636 -0
- package/dist/utils/parallel-processor.js.map +1 -0
- package/dist/utils/python.d.ts +8 -0
- package/dist/utils/python.d.ts.map +1 -0
- package/dist/utils/python.js +56 -0
- package/dist/utils/python.js.map +1 -0
- package/dist/utils/runtime.d.ts +153 -0
- package/dist/utils/runtime.d.ts.map +1 -0
- package/dist/utils/runtime.js +519 -0
- package/dist/utils/runtime.js.map +1 -0
- package/package.json +136 -0
- package/runtime/python_bridge.py +476 -0
- package/src/cli.ts +331 -0
- package/src/config/index.ts +289 -0
- package/src/core/analyzer.ts +788 -0
- package/src/core/discovery.ts +415 -0
- package/src/core/generator.ts +447 -0
- package/src/core/mapper.ts +504 -0
- package/src/core/validation.ts +693 -0
- package/src/index.ts +83 -0
- package/src/runtime/base.ts +32 -0
- package/src/runtime/errors.ts +19 -0
- package/src/runtime/http.ts +132 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/node.ts +431 -0
- package/src/runtime/optimized-node.ts +838 -0
- package/src/runtime/protocol.ts +2 -0
- package/src/runtime/pyodide.ts +228 -0
- package/src/types/global.d.ts +60 -0
- package/src/types/index.ts +410 -0
- package/src/tywrap.ts +669 -0
- package/src/utils/bundle-optimizer.ts +876 -0
- package/src/utils/cache.ts +634 -0
- package/src/utils/codec.ts +275 -0
- package/src/utils/logger.ts +201 -0
- package/src/utils/memory-profiler.ts +582 -0
- package/src/utils/parallel-processor.ts +879 -0
- package/src/utils/python.ts +79 -0
- package/src/utils/runtime.ts +616 -0
package/src/tywrap.ts
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main tywrap API entry point
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CodeGenerator } from './core/generator.js';
|
|
6
|
+
import { TypeMapper } from './core/mapper.js';
|
|
7
|
+
import { createConfig } from './config/index.js';
|
|
8
|
+
import type {
|
|
9
|
+
TywrapOptions,
|
|
10
|
+
PythonFunction,
|
|
11
|
+
PythonModule as TSPythonModule,
|
|
12
|
+
PythonClass,
|
|
13
|
+
Parameter,
|
|
14
|
+
PythonType,
|
|
15
|
+
} from './types/index.js';
|
|
16
|
+
import { fsUtils, pathUtils, processUtils, hashUtils } from './utils/runtime.js';
|
|
17
|
+
import { globalCache } from './utils/cache.js';
|
|
18
|
+
import { globalParallelProcessor } from './utils/parallel-processor.js';
|
|
19
|
+
import { resolvePythonExecutable } from './utils/python.js';
|
|
20
|
+
|
|
21
|
+
// Collect unknown typing constructs encountered during annotation parsing (per-generate run)
|
|
22
|
+
let unknownTypeNamesCollector: Map<string, number> = new Map();
|
|
23
|
+
function recordUnknown(name: string): void {
|
|
24
|
+
const prev = unknownTypeNamesCollector.get(name) ?? 0;
|
|
25
|
+
unknownTypeNamesCollector.set(name, prev + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Main tywrap function
|
|
30
|
+
*/
|
|
31
|
+
export interface TywrapInstance {
|
|
32
|
+
mapper: TypeMapper;
|
|
33
|
+
generator: CodeGenerator;
|
|
34
|
+
options: Partial<TywrapOptions>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<TywrapInstance> {
|
|
38
|
+
const mapper = new TypeMapper({ presets: options.types?.presets });
|
|
39
|
+
const generator = new CodeGenerator(mapper);
|
|
40
|
+
|
|
41
|
+
globalCache.setDebug(options.debug ?? false);
|
|
42
|
+
globalParallelProcessor.setDebug(options.debug ?? false);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
mapper,
|
|
46
|
+
generator,
|
|
47
|
+
options,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default tywrap;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Append minimal tsd tests for a generated module (optional dev aid)
|
|
55
|
+
*/
|
|
56
|
+
export async function emitTypeTestsForModule(
|
|
57
|
+
moduleName: string,
|
|
58
|
+
outDir = 'test-d/generated'
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
61
|
+
const { join } = await import('path');
|
|
62
|
+
try {
|
|
63
|
+
await mkdir(outDir, { recursive: true });
|
|
64
|
+
} catch {}
|
|
65
|
+
const filePath = join(outDir, `${moduleName}.test-d.ts`);
|
|
66
|
+
const content = `import { expectType } from 'tsd';
|
|
67
|
+
import * as mod from '../../generated/${moduleName}.generated.ts';
|
|
68
|
+
|
|
69
|
+
// Opportunistic: ensure module namespace is an object
|
|
70
|
+
expectType<Record<string, unknown>>(mod);
|
|
71
|
+
`;
|
|
72
|
+
await writeFile(filePath, content, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate TypeScript wrappers for configured Python modules.
|
|
77
|
+
* Minimal MVP implementation: resolves module file, analyzes, generates TS, writes to output.dir
|
|
78
|
+
*/
|
|
79
|
+
export async function generate(
|
|
80
|
+
options: Partial<TywrapOptions>
|
|
81
|
+
): Promise<{ written: string[]; warnings: string[] }> {
|
|
82
|
+
const resolvedOptions = createConfig(options);
|
|
83
|
+
const instance = await tywrap(resolvedOptions);
|
|
84
|
+
const written: string[] = [];
|
|
85
|
+
const warnings: string[] = [];
|
|
86
|
+
const outputDir = resolvedOptions.output.dir;
|
|
87
|
+
const caching = resolvedOptions.performance.caching;
|
|
88
|
+
const cacheDir = '.tywrap/cache';
|
|
89
|
+
const pythonPath = await resolvePythonExecutable({
|
|
90
|
+
pythonPath: resolvedOptions.runtime?.node?.pythonPath,
|
|
91
|
+
virtualEnv: resolvedOptions.runtime?.node?.virtualEnv,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Ensure directory exists (Node-only best-effort)
|
|
95
|
+
try {
|
|
96
|
+
const modFs = await import('fs/promises');
|
|
97
|
+
await modFs.mkdir(outputDir, { recursive: true });
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore in non-node or if already exists
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const modules = resolvedOptions.pythonModules ?? {};
|
|
103
|
+
for (const entry of Object.entries(modules)) {
|
|
104
|
+
const moduleKey = entry[0];
|
|
105
|
+
// reset collector for each module
|
|
106
|
+
unknownTypeNamesCollector = new Map();
|
|
107
|
+
// Prefer Python IR extractor over TS analyzer with optional cache
|
|
108
|
+
const cacheKey = await computeCacheKey(moduleKey, options);
|
|
109
|
+
let ir: unknown | null = null;
|
|
110
|
+
let irError: string | undefined;
|
|
111
|
+
if (caching && fsUtils.isAvailable()) {
|
|
112
|
+
try {
|
|
113
|
+
const modFs = await import('fs/promises');
|
|
114
|
+
await modFs.mkdir(cacheDir, { recursive: true });
|
|
115
|
+
} catch {}
|
|
116
|
+
try {
|
|
117
|
+
const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
|
|
118
|
+
ir = JSON.parse(cached);
|
|
119
|
+
} catch {
|
|
120
|
+
ir = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!ir) {
|
|
124
|
+
const fetchResult = await fetchPythonIr(moduleKey, pythonPath);
|
|
125
|
+
ir = fetchResult.ir;
|
|
126
|
+
irError = fetchResult.error;
|
|
127
|
+
if (ir && caching && fsUtils.isAvailable()) {
|
|
128
|
+
try {
|
|
129
|
+
await fsUtils.writeFile(pathUtils.join(cacheDir, cacheKey), JSON.stringify(ir));
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!ir) {
|
|
134
|
+
warnings.push(`No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const moduleModel = transformIrToTsModel(ir);
|
|
139
|
+
|
|
140
|
+
// Generate module code
|
|
141
|
+
const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
|
|
142
|
+
const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
|
|
143
|
+
|
|
144
|
+
// Write file
|
|
145
|
+
const baseName = moduleModel.name || 'module';
|
|
146
|
+
const outPath = pathUtils.join(outputDir, `${baseName}.generated.ts`);
|
|
147
|
+
await fsUtils.writeFile(outPath, gen.typescript);
|
|
148
|
+
written.push(outPath);
|
|
149
|
+
|
|
150
|
+
// Optional .d.ts emission (header-only declarations mirroring exports)
|
|
151
|
+
if (resolvedOptions.output?.declaration) {
|
|
152
|
+
const dtsPath = pathUtils.join(outputDir, `${baseName}.generated.d.ts`);
|
|
153
|
+
const dts = renderDts(gen.typescript);
|
|
154
|
+
await fsUtils.writeFile(dtsPath, dts);
|
|
155
|
+
written.push(dtsPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Optional source map emission (placeholder mapping for now)
|
|
159
|
+
if (resolvedOptions.output?.sourceMap) {
|
|
160
|
+
const mapPath = pathUtils.join(outputDir, `${baseName}.generated.ts.map`);
|
|
161
|
+
const map = renderSourceMapPlaceholder(moduleModel.name);
|
|
162
|
+
await fsUtils.writeFile(mapPath, map);
|
|
163
|
+
written.push(mapPath);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Emit warning summary of unknown typing constructs (best-effort)
|
|
167
|
+
if (unknownTypeNamesCollector.size > 0) {
|
|
168
|
+
const entries = Array.from(unknownTypeNamesCollector.entries()).sort((a, b) => b[1] - a[1]);
|
|
169
|
+
const unkList = entries
|
|
170
|
+
.slice(0, 25)
|
|
171
|
+
.map(([n, c]) => `${n}:${c}`)
|
|
172
|
+
.join(', ');
|
|
173
|
+
warnings.push(
|
|
174
|
+
`Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
|
|
175
|
+
);
|
|
176
|
+
// Write JSON report
|
|
177
|
+
try {
|
|
178
|
+
const reportsDir = pathUtils.join('.tywrap', 'reports');
|
|
179
|
+
await fsUtils.writeFile(
|
|
180
|
+
pathUtils.join(reportsDir, `${baseName}.json`),
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
module: baseName,
|
|
183
|
+
unknowns: Object.fromEntries(entries),
|
|
184
|
+
generatedAt: new Date().toISOString(),
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
} catch {
|
|
188
|
+
// ignore
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { written, warnings };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Invoke the Python IR CLI to get JSON IR for a module.
|
|
198
|
+
*/
|
|
199
|
+
async function fetchPythonIr(
|
|
200
|
+
moduleName: string,
|
|
201
|
+
pythonPath: string
|
|
202
|
+
): Promise<{ ir: unknown | null; error?: string }> {
|
|
203
|
+
if (!processUtils.isAvailable()) {
|
|
204
|
+
return { ir: null, error: 'Subprocess operations not available in this runtime' };
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const result = await processUtils.exec(pythonPath, [
|
|
208
|
+
'-m',
|
|
209
|
+
'tywrap_ir',
|
|
210
|
+
'--module',
|
|
211
|
+
moduleName,
|
|
212
|
+
'--no-pretty',
|
|
213
|
+
]);
|
|
214
|
+
if (result.code === 0) {
|
|
215
|
+
try {
|
|
216
|
+
return { ir: JSON.parse(result.stdout) };
|
|
217
|
+
} catch {
|
|
218
|
+
return {
|
|
219
|
+
ir: null,
|
|
220
|
+
error: `Failed to parse tywrap_ir output. stderr: ${result.stderr.trim() || 'empty'}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback to invoking local __main__.py
|
|
226
|
+
const localMain = pathUtils.join(process.cwd(), 'tywrap_ir', 'tywrap_ir', '__main__.py');
|
|
227
|
+
const fallback = await processUtils.exec(pythonPath, [
|
|
228
|
+
localMain,
|
|
229
|
+
'--module',
|
|
230
|
+
moduleName,
|
|
231
|
+
'--no-pretty',
|
|
232
|
+
]);
|
|
233
|
+
if (fallback.code !== 0) {
|
|
234
|
+
return {
|
|
235
|
+
ir: null,
|
|
236
|
+
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || result.stderr.trim() || 'empty'}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return { ir: JSON.parse(fallback.stdout) };
|
|
241
|
+
} catch {
|
|
242
|
+
return {
|
|
243
|
+
ir: null,
|
|
244
|
+
error: `Failed to parse tywrap_ir fallback output. stderr: ${fallback.stderr.trim() || 'empty'}`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return { ir: null, error: err instanceof Error ? err.message : String(err) };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Convert JSON IR from Python into the internal TypeScript model used by the generator.
|
|
254
|
+
*/
|
|
255
|
+
function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
256
|
+
const obj: Record<string, unknown> =
|
|
257
|
+
typeof ir === 'object' && ir !== null ? (ir as Record<string, unknown>) : {};
|
|
258
|
+
const functions = (obj.functions as unknown[]) ?? [];
|
|
259
|
+
const classes = (obj.classes as unknown[]) ?? [];
|
|
260
|
+
const mapParam = (p: Record<string, unknown>): Parameter => ({
|
|
261
|
+
name: String(p.name ?? ''),
|
|
262
|
+
type: parseAnnotationToPythonType(p.annotation),
|
|
263
|
+
optional: Boolean(p.default),
|
|
264
|
+
varArgs: p.kind === 'VAR_POSITIONAL',
|
|
265
|
+
kwArgs: p.kind === 'VAR_KEYWORD',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const mapFunc = (f: Record<string, unknown>): PythonFunction => ({
|
|
269
|
+
name: String(f.name ?? ''),
|
|
270
|
+
signature: {
|
|
271
|
+
parameters: Array.isArray(f.parameters)
|
|
272
|
+
? (f.parameters as unknown[]).map(v => mapParam((v ?? {}) as Record<string, unknown>))
|
|
273
|
+
: [],
|
|
274
|
+
returnType: parseAnnotationToPythonType(f.returns),
|
|
275
|
+
isAsync: Boolean(f.is_async),
|
|
276
|
+
isGenerator: Boolean(f.is_generator),
|
|
277
|
+
},
|
|
278
|
+
docstring: (f.docstring as string | undefined) ?? undefined,
|
|
279
|
+
decorators: [],
|
|
280
|
+
isAsync: Boolean(f.is_async),
|
|
281
|
+
isGenerator: Boolean(f.is_generator),
|
|
282
|
+
returnType: parseAnnotationToPythonType(f.returns),
|
|
283
|
+
parameters: Array.isArray(f.parameters)
|
|
284
|
+
? (f.parameters as unknown[]).map(v => mapParam((v ?? {}) as Record<string, unknown>))
|
|
285
|
+
: [],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const mapClass = (c: Record<string, unknown>): PythonClass => ({
|
|
289
|
+
name: String(c.name ?? ''),
|
|
290
|
+
bases: Array.isArray(c.bases) ? (c.bases as string[]) : [],
|
|
291
|
+
methods: Array.isArray(c.methods)
|
|
292
|
+
? (c.methods as unknown[]).map(v => mapFunc((v ?? {}) as Record<string, unknown>))
|
|
293
|
+
: [],
|
|
294
|
+
properties: Array.isArray(c.fields)
|
|
295
|
+
? ((c.fields as unknown[]).map(v => {
|
|
296
|
+
const p = (v ?? {}) as Record<string, unknown>;
|
|
297
|
+
const optional = Boolean(p.default);
|
|
298
|
+
return {
|
|
299
|
+
name: String(p.name ?? ''),
|
|
300
|
+
type: parseAnnotationToPythonType(p.annotation),
|
|
301
|
+
readonly: false,
|
|
302
|
+
setter: false,
|
|
303
|
+
getter: true,
|
|
304
|
+
optional,
|
|
305
|
+
} as unknown as never;
|
|
306
|
+
}) as unknown as PythonClass['properties'])
|
|
307
|
+
: [],
|
|
308
|
+
docstring: (c.docstring as string | undefined) ?? undefined,
|
|
309
|
+
decorators: (c.typed_dict as boolean) ? ['__typed_dict__'] : [],
|
|
310
|
+
kind: (c.typed_dict as boolean)
|
|
311
|
+
? 'typed_dict'
|
|
312
|
+
: (c.is_protocol as boolean)
|
|
313
|
+
? 'protocol'
|
|
314
|
+
: (c.is_namedtuple as boolean)
|
|
315
|
+
? 'namedtuple'
|
|
316
|
+
: (c.is_dataclass as boolean)
|
|
317
|
+
? 'dataclass'
|
|
318
|
+
: (c.is_pydantic as boolean)
|
|
319
|
+
? 'pydantic'
|
|
320
|
+
: 'class',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const moduleModel: TSPythonModule = {
|
|
324
|
+
name: (obj.module as string) ?? 'module',
|
|
325
|
+
path: undefined,
|
|
326
|
+
version:
|
|
327
|
+
typeof (obj.metadata as Record<string, unknown> | undefined)?.package_version === 'string'
|
|
328
|
+
? ((obj.metadata as Record<string, unknown> | undefined)?.package_version as string)
|
|
329
|
+
: undefined,
|
|
330
|
+
functions: functions.map(v => mapFunc((v ?? {}) as Record<string, unknown>)),
|
|
331
|
+
classes: classes.map(v => mapClass((v ?? {}) as Record<string, unknown>)),
|
|
332
|
+
imports: [],
|
|
333
|
+
exports: [],
|
|
334
|
+
};
|
|
335
|
+
return moduleModel;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function unknownType(): PythonType {
|
|
339
|
+
return { kind: 'custom', name: 'Any', module: 'typing' };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Parse Python IR annotation string -> PythonType
|
|
343
|
+
function parseAnnotationToPythonType(annotation: unknown, depth = 0): PythonType {
|
|
344
|
+
if (annotation === null || annotation === undefined) {
|
|
345
|
+
return unknownType();
|
|
346
|
+
}
|
|
347
|
+
if (depth > 100) {
|
|
348
|
+
return unknownType();
|
|
349
|
+
}
|
|
350
|
+
const raw = String(annotation).trim();
|
|
351
|
+
|
|
352
|
+
// Handle built-in class repr: <class 'int'>
|
|
353
|
+
const classMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
|
|
354
|
+
if (classMatch) {
|
|
355
|
+
const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
|
|
356
|
+
const name = (inner.split('.').pop() ?? '').toString();
|
|
357
|
+
return mapSimpleName(name);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// PEP 604 unions: int | str | None
|
|
361
|
+
if (raw.includes(' | ')) {
|
|
362
|
+
const parts = splitTopLevel(raw, '|');
|
|
363
|
+
const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
364
|
+
return { kind: 'union', types };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// typing.Union[...]
|
|
368
|
+
if (raw.startsWith('typing.Union[') || raw.startsWith('Union[')) {
|
|
369
|
+
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
370
|
+
const parts = splitTopLevel(inner, ',');
|
|
371
|
+
const types = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
372
|
+
return { kind: 'union', types };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Optional[T]
|
|
376
|
+
if (raw.startsWith('typing.Optional[') || raw.startsWith('Optional[')) {
|
|
377
|
+
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
378
|
+
const base = parseAnnotationToPythonType(inner, depth + 1);
|
|
379
|
+
return { kind: 'optional', type: base };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Literal[...] -> literal values union
|
|
383
|
+
if (raw.startsWith('typing.Literal[') || raw.startsWith('Literal[')) {
|
|
384
|
+
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
385
|
+
const parts = splitTopLevel(inner, ',');
|
|
386
|
+
if (parts.length === 1) {
|
|
387
|
+
return mapLiteral(String(parts[0] ?? '').trim());
|
|
388
|
+
}
|
|
389
|
+
return { kind: 'union', types: parts.map(p => mapLiteral(String(p).trim())) } as PythonType;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// typing_extensions wrappers: ClassVar[T], Final[T], Required[T], NotRequired[T]
|
|
393
|
+
const extMatch = raw.match(
|
|
394
|
+
/^(typing\.|typing_extensions\.)?(ClassVar|Final|Required|NotRequired)\[(.*)\]$/
|
|
395
|
+
);
|
|
396
|
+
if (extMatch) {
|
|
397
|
+
const inner = extMatch[3] ?? '';
|
|
398
|
+
const base = parseAnnotationToPythonType(inner, depth + 1);
|
|
399
|
+
return base;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// LiteralString
|
|
403
|
+
if (raw === 'typing.LiteralString' || raw === 'LiteralString') {
|
|
404
|
+
return { kind: 'primitive', name: 'str' } as PythonType;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Callable[[...], R]
|
|
408
|
+
if (raw.startsWith('typing.Callable[') || raw.startsWith('Callable[')) {
|
|
409
|
+
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
410
|
+
const parts = splitTopLevel(inner, ',');
|
|
411
|
+
if (parts.length >= 2) {
|
|
412
|
+
const paramsPart = (parts[0] ?? '').trim();
|
|
413
|
+
const returnPart = parts.slice(1).join(',').trim();
|
|
414
|
+
const paramInner =
|
|
415
|
+
paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
|
|
416
|
+
const paramTypes = ((): PythonType[] => {
|
|
417
|
+
const trimmed = paramInner.trim();
|
|
418
|
+
if (trimmed === '...' || trimmed === 'Ellipsis') {
|
|
419
|
+
return [{ kind: 'custom', name: '...' } as PythonType];
|
|
420
|
+
}
|
|
421
|
+
return trimmed
|
|
422
|
+
? splitTopLevel(trimmed, ',').map(p => parseAnnotationToPythonType(p.trim(), depth + 1))
|
|
423
|
+
: [];
|
|
424
|
+
})();
|
|
425
|
+
const returnType = parseAnnotationToPythonType(returnPart, depth + 1);
|
|
426
|
+
return { kind: 'callable', parameters: paramTypes, returnType } as PythonType;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Mapping[K, V] / Dict[K, V] normalization
|
|
431
|
+
if (raw.startsWith('typing.Mapping[') || raw.startsWith('Mapping[')) {
|
|
432
|
+
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
433
|
+
const parts = splitTopLevel(inner, ',');
|
|
434
|
+
const itemTypes = parts.map(p => parseAnnotationToPythonType(p.trim(), depth + 1));
|
|
435
|
+
return { kind: 'collection', name: 'dict', itemTypes } as PythonType;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Annotated[T, ...] -> annotated node with base and metadata
|
|
439
|
+
if (raw.startsWith('typing.Annotated[') || raw.startsWith('Annotated[')) {
|
|
440
|
+
const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
441
|
+
const parts = splitTopLevel(inner, ',');
|
|
442
|
+
if (parts.length > 0) {
|
|
443
|
+
const base = parseAnnotationToPythonType((parts[0] ?? '').trim(), depth + 1);
|
|
444
|
+
const metaParts = parts.slice(1).map(p => String(p).trim());
|
|
445
|
+
return { kind: 'annotated', base, metadata: metaParts } as PythonType;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Collections: list[T], dict[K,V], tuple[...], set[T]
|
|
450
|
+
const coll = normalizeCollectionName(raw);
|
|
451
|
+
if (coll) {
|
|
452
|
+
const { name, inner } = coll;
|
|
453
|
+
const itemParts =
|
|
454
|
+
name === 'dict' ? splitTopLevel(inner ?? '', ',') : splitTopLevel(inner ?? '', ',');
|
|
455
|
+
const itemTypes = (inner ? itemParts : []).map(p =>
|
|
456
|
+
parseAnnotationToPythonType(p.trim(), depth + 1)
|
|
457
|
+
);
|
|
458
|
+
return { kind: 'collection', name, itemTypes };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// typing.Callable[...] unlikely for now - treat as custom
|
|
462
|
+
|
|
463
|
+
// Bare names like int, str, float, bool, bytes, None
|
|
464
|
+
return mapSimpleName(raw);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function mapSimpleName(name: string): PythonType {
|
|
468
|
+
const n = name.replace(/^typing\./, '').trim();
|
|
469
|
+
if (n === 'int' || n === 'float' || n === 'str' || n === 'bool' || n === 'bytes') {
|
|
470
|
+
// cast to the specific union type without using any
|
|
471
|
+
return { kind: 'primitive', name: n };
|
|
472
|
+
}
|
|
473
|
+
// Track unknown typing-ish names for diagnostics
|
|
474
|
+
if (
|
|
475
|
+
n === 'Any' ||
|
|
476
|
+
n === 'Never' ||
|
|
477
|
+
n === 'LiteralString' ||
|
|
478
|
+
n === 'ClassVar' ||
|
|
479
|
+
n === 'Final' ||
|
|
480
|
+
n === 'TypeAlias' ||
|
|
481
|
+
n === 'Required' ||
|
|
482
|
+
n === 'NotRequired'
|
|
483
|
+
) {
|
|
484
|
+
try {
|
|
485
|
+
recordUnknown(n);
|
|
486
|
+
} catch {}
|
|
487
|
+
}
|
|
488
|
+
if (n === 'None' || n.toLowerCase() === 'nonetype') {
|
|
489
|
+
return { kind: 'primitive', name: 'None' };
|
|
490
|
+
}
|
|
491
|
+
if (n === 'list' || n === 'List') {
|
|
492
|
+
return { kind: 'collection', name: 'list', itemTypes: [] };
|
|
493
|
+
}
|
|
494
|
+
if (n === 'dict' || n === 'Dict') {
|
|
495
|
+
return { kind: 'collection', name: 'dict', itemTypes: [] };
|
|
496
|
+
}
|
|
497
|
+
if (n === 'tuple' || n === 'Tuple') {
|
|
498
|
+
return { kind: 'collection', name: 'tuple', itemTypes: [] };
|
|
499
|
+
}
|
|
500
|
+
if (n === 'set' || n === 'Set') {
|
|
501
|
+
return { kind: 'collection', name: 'set', itemTypes: [] };
|
|
502
|
+
}
|
|
503
|
+
return { kind: 'custom', name: n };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function normalizeCollectionName(
|
|
507
|
+
raw: string
|
|
508
|
+
): { name: 'list' | 'dict' | 'tuple' | 'set' | 'frozenset'; inner?: string } | null {
|
|
509
|
+
const m = raw.match(/^(typing\.)?(List|Dict|Tuple|Set|list|dict|tuple|set)\[(.*)\]$/);
|
|
510
|
+
if (!m) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const nameRaw = String(m[2] ?? '');
|
|
514
|
+
const name =
|
|
515
|
+
nameRaw.toLowerCase() === 'list'
|
|
516
|
+
? 'list'
|
|
517
|
+
: nameRaw.toLowerCase() === 'dict'
|
|
518
|
+
? 'dict'
|
|
519
|
+
: nameRaw.toLowerCase() === 'tuple'
|
|
520
|
+
? 'tuple'
|
|
521
|
+
: 'set';
|
|
522
|
+
return { name, inner: String(m[3] ?? '') };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function splitTopLevel(input: string, sep: '|' | ','): string[] {
|
|
526
|
+
const results: string[] = [];
|
|
527
|
+
let level = 0;
|
|
528
|
+
let cur = '';
|
|
529
|
+
let guard = 0;
|
|
530
|
+
for (let i = 0; i < input.length; i++) {
|
|
531
|
+
// Depth/length guard to avoid pathological recursion/loops
|
|
532
|
+
guard++;
|
|
533
|
+
if (guard > 20000) {
|
|
534
|
+
// Bail out safely; push remaining and break
|
|
535
|
+
if (cur.trim()) {
|
|
536
|
+
results.push(cur.trim());
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
const ch = input.charAt(i);
|
|
541
|
+
if (ch === '[' || ch === '(') {
|
|
542
|
+
level++;
|
|
543
|
+
}
|
|
544
|
+
if (ch === ']' || ch === ')') {
|
|
545
|
+
level = Math.max(0, level - 1);
|
|
546
|
+
}
|
|
547
|
+
if (level === 0 && input.slice(i, i + sep.length) === sep) {
|
|
548
|
+
results.push(cur.trim());
|
|
549
|
+
cur = '';
|
|
550
|
+
i += sep.length - 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
cur += ch;
|
|
554
|
+
}
|
|
555
|
+
if (cur.trim()) {
|
|
556
|
+
results.push(cur.trim());
|
|
557
|
+
}
|
|
558
|
+
return results;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function mapLiteral(text: string): PythonType {
|
|
562
|
+
// strip quotes if present
|
|
563
|
+
const t = text.trim();
|
|
564
|
+
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
|
565
|
+
return { kind: 'literal', value: t.slice(1, -1) } as PythonType;
|
|
566
|
+
}
|
|
567
|
+
if (t === 'True' || t === 'False') {
|
|
568
|
+
return { kind: 'literal', value: t === 'True' } as PythonType;
|
|
569
|
+
}
|
|
570
|
+
if (t === 'None') {
|
|
571
|
+
return { kind: 'literal', value: null } as PythonType;
|
|
572
|
+
}
|
|
573
|
+
const num = Number(t);
|
|
574
|
+
if (!Number.isNaN(num)) {
|
|
575
|
+
return { kind: 'literal', value: num } as PythonType;
|
|
576
|
+
}
|
|
577
|
+
// fallback
|
|
578
|
+
return { kind: 'custom', name: t } as PythonType;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Compute a stable cache key filename for a module and options
|
|
583
|
+
*/
|
|
584
|
+
async function computeCacheKey(
|
|
585
|
+
moduleName: string,
|
|
586
|
+
options: Partial<TywrapOptions>
|
|
587
|
+
): Promise<string> {
|
|
588
|
+
const modules = options.pythonModules ?? {};
|
|
589
|
+
const foundEntry = Object.entries(modules).find(([name]) => name === moduleName);
|
|
590
|
+
const moduleConfig = foundEntry ? foundEntry[1] : undefined;
|
|
591
|
+
const runtimePython = await resolvePythonExecutable({
|
|
592
|
+
pythonPath: options.runtime?.node?.pythonPath,
|
|
593
|
+
virtualEnv: options.runtime?.node?.virtualEnv,
|
|
594
|
+
});
|
|
595
|
+
const keyObject = {
|
|
596
|
+
module: moduleName,
|
|
597
|
+
moduleVersion: moduleConfig?.version ?? null,
|
|
598
|
+
runtime: {
|
|
599
|
+
pythonPath: runtimePython,
|
|
600
|
+
virtualEnv: options.runtime?.node?.virtualEnv ?? null,
|
|
601
|
+
},
|
|
602
|
+
output: {
|
|
603
|
+
format: options.output?.format ?? 'esm',
|
|
604
|
+
declaration: options.output?.declaration ?? false,
|
|
605
|
+
sourceMap: options.output?.sourceMap ?? false,
|
|
606
|
+
},
|
|
607
|
+
performance: {
|
|
608
|
+
caching: options.performance?.caching ?? false,
|
|
609
|
+
compression: options.performance?.compression ?? 'none',
|
|
610
|
+
},
|
|
611
|
+
typeHints: moduleConfig?.typeHints ?? 'strict',
|
|
612
|
+
} as const;
|
|
613
|
+
const normalized = JSON.stringify(keyObject);
|
|
614
|
+
const digest = await hashUtils.sha256Hex(normalized);
|
|
615
|
+
return `ir_${moduleName}_${digest.slice(0, 16)}.json`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Very lightweight .d.ts emitter derived from generated TS wrappers
|
|
620
|
+
* This is intentionally minimal and stable for our wrappers shape.
|
|
621
|
+
*/
|
|
622
|
+
function renderDts(generatedTs: string): string {
|
|
623
|
+
const header = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
|
|
624
|
+
const lines: string[] = [header];
|
|
625
|
+
// Extract function exports
|
|
626
|
+
const funcRegex = /export\s+async\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
|
|
627
|
+
let m: RegExpExecArray | null;
|
|
628
|
+
while ((m = funcRegex.exec(generatedTs)) !== null) {
|
|
629
|
+
const name = m[1];
|
|
630
|
+
const params = m[2];
|
|
631
|
+
const ret = m[3];
|
|
632
|
+
lines.push(`export function ${name}(${params}): Promise<${ret}>;`);
|
|
633
|
+
}
|
|
634
|
+
// Extract class exports and methods
|
|
635
|
+
const classRegex = /export\s+class\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
636
|
+
while ((m = classRegex.exec(generatedTs)) !== null) {
|
|
637
|
+
const className = String(m[1] ?? '');
|
|
638
|
+
const body = String(m[2] ?? '');
|
|
639
|
+
// Constructor is always variadic unknown[] in current generator
|
|
640
|
+
const methods: string[] = [];
|
|
641
|
+
const methodRegex = /\n\s+async\s+(\w+)\s*\(([^)]*)\)\s*:\s*Promise<([^>]+)>/g;
|
|
642
|
+
let mm: RegExpExecArray | null;
|
|
643
|
+
while ((mm = methodRegex.exec(body)) !== null) {
|
|
644
|
+
methods.push(` ${mm[1]}(${mm[2]}): Promise<${mm[3]}>;`);
|
|
645
|
+
}
|
|
646
|
+
lines.push(`export class ${className} {`);
|
|
647
|
+
lines.push(` constructor(...args: unknown[]);`);
|
|
648
|
+
if (methods.length > 0) {
|
|
649
|
+
lines.push(methods.join('\n'));
|
|
650
|
+
}
|
|
651
|
+
lines.push('}');
|
|
652
|
+
}
|
|
653
|
+
return `${lines.join('\n')}\n`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Minimal source map placeholder (stable, empty mappings)
|
|
658
|
+
*/
|
|
659
|
+
function renderSourceMapPlaceholder(moduleName: string | undefined): string {
|
|
660
|
+
const safe = moduleName ?? 'module';
|
|
661
|
+
const map = {
|
|
662
|
+
version: 3,
|
|
663
|
+
file: `${safe}.generated.ts`,
|
|
664
|
+
sources: [],
|
|
665
|
+
names: [],
|
|
666
|
+
mappings: '',
|
|
667
|
+
} as const;
|
|
668
|
+
return JSON.stringify(map, null, 0);
|
|
669
|
+
}
|