tywrap 0.6.1 → 0.8.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 +15 -5
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/emit-call.d.ts.map +1 -1
- package/dist/core/emit-call.js +1 -1
- package/dist/core/emit-call.js.map +1 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +40 -9
- package/dist/core/generator.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +1 -3
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +57 -0
- package/dist/runtime/base-bridge.d.ts.map +1 -0
- package/dist/runtime/base-bridge.js +72 -0
- package/dist/runtime/base-bridge.js.map +1 -0
- package/dist/runtime/frame-codec.d.ts +111 -0
- package/dist/runtime/frame-codec.d.ts.map +1 -0
- package/dist/runtime/frame-codec.js +352 -0
- package/dist/runtime/frame-codec.js.map +1 -0
- package/dist/runtime/http-transport.d.ts +11 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +19 -0
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/http.d.ts +5 -12
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -29
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +25 -19
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +19 -34
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +21 -2
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +16 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +12 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +20 -0
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/pyodide.d.ts +5 -12
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +6 -29
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +14 -1
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +68 -6
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +177 -3
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +526 -26
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/transport.d.ts +141 -0
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +21 -0
- package/dist/runtime/transport.js.map +1 -1
- package/dist/types/index.d.ts +59 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +204 -149
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/codec.d.ts +2 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +205 -6
- package/dist/utils/codec.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -1
- package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/frame_codec.py +424 -0
- package/runtime/python_bridge.py +241 -42
- package/runtime/tywrap_bridge_core.py +152 -13
- package/src/core/annotation-parser.ts +2 -1
- package/src/core/emit-call.ts +1 -7
- package/src/core/generator.ts +50 -11
- package/src/dev.ts +1 -3
- package/src/index.ts +1 -0
- package/src/runtime/base-bridge.ts +106 -0
- package/src/runtime/frame-codec.ts +469 -0
- package/src/runtime/http-transport.ts +21 -1
- package/src/runtime/http.ts +7 -51
- package/src/runtime/index.ts +2 -6
- package/src/runtime/node.ts +42 -53
- package/src/runtime/pooled-transport.ts +25 -2
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +22 -0
- package/src/runtime/pyodide.ts +7 -52
- package/src/runtime/rpc-client.ts +91 -7
- package/src/runtime/subprocess-transport.ts +629 -30
- package/src/runtime/transport.ts +169 -0
- package/src/types/index.ts +62 -0
- package/src/tywrap.ts +265 -162
- package/src/utils/codec.ts +245 -7
- package/src/version.ts +1 -1
package/src/tywrap.ts
CHANGED
|
@@ -15,13 +15,14 @@ import type {
|
|
|
15
15
|
PythonTypeAlias,
|
|
16
16
|
Parameter,
|
|
17
17
|
PythonType,
|
|
18
|
+
PythonModuleConfig,
|
|
18
19
|
} from './types/index.js';
|
|
19
20
|
import { fsUtils, pathUtils, processUtils, isWindows } from './utils/runtime.js';
|
|
20
21
|
import { globalCache } from './utils/cache.js';
|
|
21
22
|
import { resolvePythonExecutable } from './utils/python.js';
|
|
22
23
|
import { computeIrCacheFilename } from './utils/ir-cache.js';
|
|
23
24
|
|
|
24
|
-
const TYWRAP_IR_VERSION = '0.
|
|
25
|
+
const TYWRAP_IR_VERSION = '0.3.0';
|
|
25
26
|
|
|
26
27
|
// Collect unknown typing constructs encountered during annotation parsing (per-generate run)
|
|
27
28
|
let unknownTypeNamesCollector: Map<string, number> = new Map();
|
|
@@ -90,6 +91,195 @@ async function safeReadFile(path: string): Promise<string | null> {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
const CACHE_DIR = '.tywrap/cache';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve module IR via the Python extractor, consulting and (best-effort)
|
|
98
|
+
* populating the on-disk cache. Mirrors the original inline cache-then-fetch
|
|
99
|
+
* logic exactly: the cache is only read/written when caching is enabled, the
|
|
100
|
+
* filesystem is available, and we are not in check mode.
|
|
101
|
+
*/
|
|
102
|
+
async function fetchAndCacheIr(
|
|
103
|
+
moduleKey: string,
|
|
104
|
+
resolvedOptions: ReturnType<typeof createConfig>,
|
|
105
|
+
pythonPath: string,
|
|
106
|
+
cacheKey: string,
|
|
107
|
+
caching: boolean,
|
|
108
|
+
checkMode: boolean
|
|
109
|
+
): Promise<{ ir: unknown | null; error?: string }> {
|
|
110
|
+
let ir: unknown | null = null;
|
|
111
|
+
let irError: string | undefined;
|
|
112
|
+
if (caching && fsUtils.isAvailable() && !checkMode) {
|
|
113
|
+
try {
|
|
114
|
+
const cached = await fsUtils.readFile(pathUtils.join(CACHE_DIR, cacheKey));
|
|
115
|
+
ir = JSON.parse(cached);
|
|
116
|
+
} catch {
|
|
117
|
+
ir = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!ir) {
|
|
121
|
+
const fetchResult = await fetchPythonIr(moduleKey, pythonPath, {
|
|
122
|
+
timeoutMs: resolvedOptions.runtime?.node?.timeout,
|
|
123
|
+
pythonImportPath: resolvedOptions.pythonImportPath,
|
|
124
|
+
});
|
|
125
|
+
ir = fetchResult.ir;
|
|
126
|
+
irError = fetchResult.error;
|
|
127
|
+
if (ir && caching && fsUtils.isAvailable() && !checkMode) {
|
|
128
|
+
try {
|
|
129
|
+
await fsUtils.writeFile(pathUtils.join(CACHE_DIR, cacheKey), JSON.stringify(ir));
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { ir, error: irError };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Apply module-level export filtering (functions/classes + excludes) in place.
|
|
138
|
+
* Pushes any filtering warnings onto the provided `warnings` array. Behavior is
|
|
139
|
+
* identical to the original inline block.
|
|
140
|
+
*/
|
|
141
|
+
function filterModuleExports(
|
|
142
|
+
moduleModel: TSPythonModule,
|
|
143
|
+
moduleConfig: PythonModuleConfig,
|
|
144
|
+
moduleKey: string,
|
|
145
|
+
warnings: string[]
|
|
146
|
+
): void {
|
|
147
|
+
const builtInDefaultExcludes = new Set([
|
|
148
|
+
'dataclass',
|
|
149
|
+
'property',
|
|
150
|
+
'staticmethod',
|
|
151
|
+
'classmethod',
|
|
152
|
+
'abstractmethod',
|
|
153
|
+
'cached_property',
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const excludeExact = new Set((moduleConfig.exclude ?? []).map(String));
|
|
157
|
+
const excludeRegexes: RegExp[] = [];
|
|
158
|
+
for (const pattern of moduleConfig.excludePatterns ?? []) {
|
|
159
|
+
try {
|
|
160
|
+
excludeRegexes.push(new RegExp(String(pattern)));
|
|
161
|
+
} catch (err: unknown) {
|
|
162
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
163
|
+
warnings.push(
|
|
164
|
+
`Module ${moduleKey}: invalid excludePatterns regex "${String(pattern)}": ${message}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const shouldExclude = (name: string, applyBuiltInDefaults: boolean): boolean => {
|
|
170
|
+
if (excludeExact.has(name)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (excludeRegexes.some(r => r.test(name))) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (applyBuiltInDefaults && builtInDefaultExcludes.has(name)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (Array.isArray(moduleConfig.functions)) {
|
|
183
|
+
const allow = new Set(moduleConfig.functions.map(String));
|
|
184
|
+
for (const requested of allow) {
|
|
185
|
+
if (!moduleModel.functions.some(f => f.name === requested)) {
|
|
186
|
+
warnings.push(`Module ${moduleKey}: configured function "${requested}" not found in IR`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
moduleModel.functions = moduleModel.functions.filter(
|
|
190
|
+
f => allow.has(f.name) && !shouldExclude(f.name, false)
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
moduleModel.functions = moduleModel.functions.filter(f => !shouldExclude(f.name, true));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(moduleConfig.classes)) {
|
|
197
|
+
const allow = new Set(moduleConfig.classes.map(String));
|
|
198
|
+
for (const requested of allow) {
|
|
199
|
+
if (!moduleModel.classes.some(c => c.name === requested)) {
|
|
200
|
+
warnings.push(`Module ${moduleKey}: configured class "${requested}" not found in IR`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
moduleModel.classes = moduleModel.classes.filter(
|
|
204
|
+
c => allow.has(c.name) && !shouldExclude(c.name, false)
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
moduleModel.classes = moduleModel.classes.filter(c => !shouldExclude(c.name, true));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
moduleModel.typeAliases = (moduleModel.typeAliases ?? []).filter(
|
|
211
|
+
alias => !shouldExclude(alias.name, false)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Either compare the generated files against disk (check mode) or write them.
|
|
217
|
+
* Mutates `written`/`outOfDate` exactly as the original inline branches did.
|
|
218
|
+
*/
|
|
219
|
+
async function emitOrCompareFiles(
|
|
220
|
+
filesToEmit: Array<{ path: string; content: string }>,
|
|
221
|
+
checkMode: boolean,
|
|
222
|
+
written: string[],
|
|
223
|
+
outOfDate: string[]
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
if (checkMode) {
|
|
226
|
+
for (const file of filesToEmit) {
|
|
227
|
+
const existing = await safeReadFile(file.path);
|
|
228
|
+
if (existing === null) {
|
|
229
|
+
outOfDate.push(file.path);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (normalizeForComparison(existing) !== normalizeForComparison(file.content)) {
|
|
233
|
+
outOfDate.push(file.path);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
for (const file of filesToEmit) {
|
|
238
|
+
await fsUtils.writeFile(file.path, file.content);
|
|
239
|
+
written.push(file.path);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Emit the best-effort warning summary of unknown typing constructs collected
|
|
246
|
+
* for the current module, and (outside check mode) write the JSON report.
|
|
247
|
+
*/
|
|
248
|
+
async function reportUnknownTypes(
|
|
249
|
+
baseName: string,
|
|
250
|
+
unknownTypeNames: Map<string, number>,
|
|
251
|
+
checkMode: boolean,
|
|
252
|
+
warnings: string[]
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
if (unknownTypeNames.size === 0) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const entries = Array.from(unknownTypeNames.entries()).sort((a, b) => b[1] - a[1]);
|
|
258
|
+
const unkList = entries
|
|
259
|
+
.slice(0, 25)
|
|
260
|
+
.map(([n, c]) => `${n}:${c}`)
|
|
261
|
+
.join(', ');
|
|
262
|
+
warnings.push(
|
|
263
|
+
`Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
|
|
264
|
+
);
|
|
265
|
+
// Write JSON report
|
|
266
|
+
if (!checkMode) {
|
|
267
|
+
try {
|
|
268
|
+
const reportsDir = pathUtils.join('.tywrap', 'reports');
|
|
269
|
+
await fsUtils.writeFile(
|
|
270
|
+
pathUtils.join(reportsDir, `${baseName}.json`),
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
module: baseName,
|
|
273
|
+
unknowns: Object.fromEntries(entries),
|
|
274
|
+
generatedAt: new Date().toISOString(),
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
} catch {
|
|
278
|
+
// ignore
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
93
283
|
/**
|
|
94
284
|
* Generate TypeScript wrappers for configured Python modules.
|
|
95
285
|
* Minimal MVP implementation: resolves module file, analyzes, generates TS, writes to output.dir
|
|
@@ -107,7 +297,6 @@ export async function generate(
|
|
|
107
297
|
const failures: GenerateFailure[] = [];
|
|
108
298
|
const outputDir = resolvedOptions.output.dir;
|
|
109
299
|
const caching = resolvedOptions.performance.caching;
|
|
110
|
-
const cacheDir = '.tywrap/cache';
|
|
111
300
|
const pythonPath = await resolvePythonExecutable({
|
|
112
301
|
pythonPath: resolvedOptions.runtime?.node?.pythonPath,
|
|
113
302
|
virtualEnv: resolvedOptions.runtime?.node?.virtualEnv,
|
|
@@ -131,29 +320,14 @@ export async function generate(
|
|
|
131
320
|
unknownTypeNamesCollector = new Map();
|
|
132
321
|
// Resolve module IR via the Python extractor (with optional cache)
|
|
133
322
|
const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (!ir) {
|
|
145
|
-
const fetchResult = await fetchPythonIr(moduleKey, pythonPath, {
|
|
146
|
-
timeoutMs: resolvedOptions.runtime?.node?.timeout,
|
|
147
|
-
pythonImportPath: resolvedOptions.pythonImportPath,
|
|
148
|
-
});
|
|
149
|
-
ir = fetchResult.ir;
|
|
150
|
-
irError = fetchResult.error;
|
|
151
|
-
if (ir && caching && fsUtils.isAvailable() && !checkMode) {
|
|
152
|
-
try {
|
|
153
|
-
await fsUtils.writeFile(pathUtils.join(cacheDir, cacheKey), JSON.stringify(ir));
|
|
154
|
-
} catch {}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
323
|
+
const { ir, error: irError } = await fetchAndCacheIr(
|
|
324
|
+
moduleKey,
|
|
325
|
+
resolvedOptions,
|
|
326
|
+
pythonPath,
|
|
327
|
+
cacheKey,
|
|
328
|
+
caching,
|
|
329
|
+
checkMode
|
|
330
|
+
);
|
|
157
331
|
if (!ir) {
|
|
158
332
|
const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
|
|
159
333
|
warnings.push(message);
|
|
@@ -168,76 +342,7 @@ export async function generate(
|
|
|
168
342
|
const moduleModel = transformIrToTsModel(ir);
|
|
169
343
|
|
|
170
344
|
// Apply module-level export filtering (functions/classes + excludes).
|
|
171
|
-
|
|
172
|
-
const builtInDefaultExcludes = new Set([
|
|
173
|
-
'dataclass',
|
|
174
|
-
'property',
|
|
175
|
-
'staticmethod',
|
|
176
|
-
'classmethod',
|
|
177
|
-
'abstractmethod',
|
|
178
|
-
'cached_property',
|
|
179
|
-
]);
|
|
180
|
-
|
|
181
|
-
const excludeExact = new Set((moduleConfig.exclude ?? []).map(String));
|
|
182
|
-
const excludeRegexes: RegExp[] = [];
|
|
183
|
-
for (const pattern of moduleConfig.excludePatterns ?? []) {
|
|
184
|
-
try {
|
|
185
|
-
excludeRegexes.push(new RegExp(String(pattern)));
|
|
186
|
-
} catch (err: unknown) {
|
|
187
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
-
warnings.push(
|
|
189
|
-
`Module ${moduleKey}: invalid excludePatterns regex "${String(pattern)}": ${message}`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const shouldExclude = (name: string, applyBuiltInDefaults: boolean): boolean => {
|
|
195
|
-
if (excludeExact.has(name)) {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
if (excludeRegexes.some(r => r.test(name))) {
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
if (applyBuiltInDefaults && builtInDefaultExcludes.has(name)) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
return false;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
if (Array.isArray(moduleConfig.functions)) {
|
|
208
|
-
const allow = new Set(moduleConfig.functions.map(String));
|
|
209
|
-
for (const requested of allow) {
|
|
210
|
-
if (!moduleModel.functions.some(f => f.name === requested)) {
|
|
211
|
-
warnings.push(
|
|
212
|
-
`Module ${moduleKey}: configured function "${requested}" not found in IR`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
moduleModel.functions = moduleModel.functions.filter(
|
|
217
|
-
f => allow.has(f.name) && !shouldExclude(f.name, false)
|
|
218
|
-
);
|
|
219
|
-
} else {
|
|
220
|
-
moduleModel.functions = moduleModel.functions.filter(f => !shouldExclude(f.name, true));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (Array.isArray(moduleConfig.classes)) {
|
|
224
|
-
const allow = new Set(moduleConfig.classes.map(String));
|
|
225
|
-
for (const requested of allow) {
|
|
226
|
-
if (!moduleModel.classes.some(c => c.name === requested)) {
|
|
227
|
-
warnings.push(`Module ${moduleKey}: configured class "${requested}" not found in IR`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
moduleModel.classes = moduleModel.classes.filter(
|
|
231
|
-
c => allow.has(c.name) && !shouldExclude(c.name, false)
|
|
232
|
-
);
|
|
233
|
-
} else {
|
|
234
|
-
moduleModel.classes = moduleModel.classes.filter(c => !shouldExclude(c.name, true));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
moduleModel.typeAliases = (moduleModel.typeAliases ?? []).filter(
|
|
238
|
-
alias => !shouldExclude(alias.name, false)
|
|
239
|
-
);
|
|
240
|
-
}
|
|
345
|
+
filterModuleExports(moduleModel, moduleConfig, moduleKey, warnings);
|
|
241
346
|
|
|
242
347
|
// Generate module code
|
|
243
348
|
const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
|
|
@@ -264,51 +369,10 @@ export async function generate(
|
|
|
264
369
|
});
|
|
265
370
|
}
|
|
266
371
|
|
|
267
|
-
|
|
268
|
-
for (const file of filesToEmit) {
|
|
269
|
-
const existing = await safeReadFile(file.path);
|
|
270
|
-
if (existing === null) {
|
|
271
|
-
outOfDate.push(file.path);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (normalizeForComparison(existing) !== normalizeForComparison(file.content)) {
|
|
275
|
-
outOfDate.push(file.path);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
for (const file of filesToEmit) {
|
|
280
|
-
await fsUtils.writeFile(file.path, file.content);
|
|
281
|
-
written.push(file.path);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
372
|
+
await emitOrCompareFiles(filesToEmit, checkMode, written, outOfDate);
|
|
284
373
|
|
|
285
374
|
// Emit warning summary of unknown typing constructs (best-effort)
|
|
286
|
-
|
|
287
|
-
const entries = Array.from(unknownTypeNamesCollector.entries()).sort((a, b) => b[1] - a[1]);
|
|
288
|
-
const unkList = entries
|
|
289
|
-
.slice(0, 25)
|
|
290
|
-
.map(([n, c]) => `${n}:${c}`)
|
|
291
|
-
.join(', ');
|
|
292
|
-
warnings.push(
|
|
293
|
-
`Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
|
|
294
|
-
);
|
|
295
|
-
// Write JSON report
|
|
296
|
-
if (!checkMode) {
|
|
297
|
-
try {
|
|
298
|
-
const reportsDir = pathUtils.join('.tywrap', 'reports');
|
|
299
|
-
await fsUtils.writeFile(
|
|
300
|
-
pathUtils.join(reportsDir, `${baseName}.json`),
|
|
301
|
-
JSON.stringify({
|
|
302
|
-
module: baseName,
|
|
303
|
-
unknowns: Object.fromEntries(entries),
|
|
304
|
-
generatedAt: new Date().toISOString(),
|
|
305
|
-
})
|
|
306
|
-
);
|
|
307
|
-
} catch {
|
|
308
|
-
// ignore
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
375
|
+
await reportUnknownTypes(baseName, unknownTypeNamesCollector, checkMode, warnings);
|
|
312
376
|
}
|
|
313
377
|
|
|
314
378
|
if (checkMode) {
|
|
@@ -318,6 +382,40 @@ export async function generate(
|
|
|
318
382
|
return { written, warnings, failures };
|
|
319
383
|
}
|
|
320
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Run the IR extractor (`python <command...>`) and parse its JSON stdout.
|
|
387
|
+
*
|
|
388
|
+
* Returns the exec result alongside a parsed-IR outcome. On a zero exit code the
|
|
389
|
+
* stdout is JSON-parsed; on parse failure the supplied `parseErrorLabel` is used
|
|
390
|
+
* to build the error message. The caller is responsible for handling non-zero
|
|
391
|
+
* exit codes (e.g. the module-missing fallback), so a non-zero exit yields
|
|
392
|
+
* `ir: null` with no `error` set here.
|
|
393
|
+
*/
|
|
394
|
+
async function execAndParseIr(
|
|
395
|
+
pythonPath: string,
|
|
396
|
+
command: string[],
|
|
397
|
+
execOptions: { timeoutMs?: number; env?: Record<string, string> },
|
|
398
|
+
parseErrorLabel: 'tywrap_ir output' | 'tywrap_ir fallback output'
|
|
399
|
+
): Promise<{
|
|
400
|
+
result: { code: number; stdout: string; stderr: string };
|
|
401
|
+
ir: unknown | null;
|
|
402
|
+
error?: string;
|
|
403
|
+
}> {
|
|
404
|
+
const result = await processUtils.exec(pythonPath, command, execOptions);
|
|
405
|
+
if (result.code !== 0) {
|
|
406
|
+
return { result, ir: null };
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
return { result, ir: JSON.parse(result.stdout) };
|
|
410
|
+
} catch {
|
|
411
|
+
return {
|
|
412
|
+
result,
|
|
413
|
+
ir: null,
|
|
414
|
+
error: `Failed to parse ${parseErrorLabel}. stderr: ${result.stderr.trim() || 'empty'}`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
321
419
|
/**
|
|
322
420
|
* Invoke the Python IR CLI to get JSON IR for a module.
|
|
323
421
|
*/
|
|
@@ -339,24 +437,19 @@ async function fetchPythonIr(
|
|
|
339
437
|
.filter(Boolean)
|
|
340
438
|
.join(delimiter);
|
|
341
439
|
const env = mergedPyPath ? { PYTHONPATH: mergedPyPath } : undefined;
|
|
440
|
+
const execOptions = { timeoutMs: options.timeoutMs, env };
|
|
342
441
|
try {
|
|
343
|
-
const
|
|
442
|
+
const primary = await execAndParseIr(
|
|
344
443
|
pythonPath,
|
|
345
444
|
['-m', 'tywrap_ir', '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
|
|
346
|
-
|
|
445
|
+
execOptions,
|
|
446
|
+
'tywrap_ir output'
|
|
347
447
|
);
|
|
348
|
-
if (result.code === 0) {
|
|
349
|
-
|
|
350
|
-
return { ir: JSON.parse(result.stdout) };
|
|
351
|
-
} catch {
|
|
352
|
-
return {
|
|
353
|
-
ir: null,
|
|
354
|
-
error: `Failed to parse tywrap_ir output. stderr: ${result.stderr.trim() || 'empty'}`,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
448
|
+
if (primary.result.code === 0) {
|
|
449
|
+
return { ir: primary.ir, error: primary.error };
|
|
357
450
|
}
|
|
358
451
|
|
|
359
|
-
const stderrText = result.stderr.trim();
|
|
452
|
+
const stderrText = primary.result.stderr.trim();
|
|
360
453
|
const isTywrapIrMissing =
|
|
361
454
|
stderrText.includes('No module named') && stderrText.includes('tywrap_ir');
|
|
362
455
|
if (!isTywrapIrMissing) {
|
|
@@ -381,25 +474,19 @@ async function fetchPythonIr(
|
|
|
381
474
|
};
|
|
382
475
|
}
|
|
383
476
|
|
|
384
|
-
const fallback = await
|
|
477
|
+
const fallback = await execAndParseIr(
|
|
385
478
|
pythonPath,
|
|
386
479
|
[localMain, '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
|
|
387
|
-
|
|
480
|
+
execOptions,
|
|
481
|
+
'tywrap_ir fallback output'
|
|
388
482
|
);
|
|
389
|
-
if (fallback.code !== 0) {
|
|
390
|
-
return {
|
|
391
|
-
ir: null,
|
|
392
|
-
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || stderrText || 'empty'}`,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
try {
|
|
396
|
-
return { ir: JSON.parse(fallback.stdout) };
|
|
397
|
-
} catch {
|
|
483
|
+
if (fallback.result.code !== 0) {
|
|
398
484
|
return {
|
|
399
485
|
ir: null,
|
|
400
|
-
error: `
|
|
486
|
+
error: `tywrap_ir failed. stderr: ${fallback.result.stderr.trim() || stderrText || 'empty'}`,
|
|
401
487
|
};
|
|
402
488
|
}
|
|
489
|
+
return { ir: fallback.ir, error: fallback.error };
|
|
403
490
|
} catch (err) {
|
|
404
491
|
return { ir: null, error: err instanceof Error ? err.message : String(err) };
|
|
405
492
|
}
|
|
@@ -483,6 +570,9 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
483
570
|
keywordOnly: p.kind === 'KEYWORD_ONLY',
|
|
484
571
|
});
|
|
485
572
|
|
|
573
|
+
const mapMethodKind = (value: unknown): PythonFunction['methodKind'] =>
|
|
574
|
+
value === 'class' || value === 'static' ? value : 'instance';
|
|
575
|
+
|
|
486
576
|
const mapFunc = (
|
|
487
577
|
f: Record<string, unknown>,
|
|
488
578
|
inheritedTypeParameters: readonly PythonGenericParameter[] = []
|
|
@@ -512,6 +602,7 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
512
602
|
mapParam((v ?? {}) as Record<string, unknown>, annotationTypeParameters)
|
|
513
603
|
)
|
|
514
604
|
: [],
|
|
605
|
+
methodKind: mapMethodKind(f.method_kind),
|
|
515
606
|
};
|
|
516
607
|
};
|
|
517
608
|
|
|
@@ -538,6 +629,18 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
538
629
|
};
|
|
539
630
|
})
|
|
540
631
|
: [],
|
|
632
|
+
accessors: Array.isArray(c.accessors)
|
|
633
|
+
? (c.accessors as unknown[]).map(v => {
|
|
634
|
+
const a = (v ?? {}) as Record<string, unknown>;
|
|
635
|
+
return {
|
|
636
|
+
name: String(a.name ?? ''),
|
|
637
|
+
type: parseType(a.returns, classTypeParameters),
|
|
638
|
+
docstring: (a.docstring as string | undefined) ?? undefined,
|
|
639
|
+
readOnly: typeof a.read_only === 'boolean' ? a.read_only : undefined,
|
|
640
|
+
isCached: Boolean(a.is_cached),
|
|
641
|
+
};
|
|
642
|
+
})
|
|
643
|
+
: [],
|
|
541
644
|
docstring: (c.docstring as string | undefined) ?? undefined,
|
|
542
645
|
decorators: (c.typed_dict as boolean) ? ['__typed_dict__'] : [],
|
|
543
646
|
kind: (c.typed_dict as boolean)
|