tywrap 0.6.0 → 0.7.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/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +19 -14
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts +17 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +59 -49
- package/dist/core/discovery.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/core/validation.d.ts +23 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +52 -48
- package/dist/core/validation.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +175 -133
- package/dist/dev.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/bridge-codec.d.ts +15 -0
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +45 -48
- package/dist/runtime/bridge-codec.js.map +1 -1
- 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 +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +12 -19
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +14 -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 +32 -1
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +45 -24
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +26 -1
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +72 -35
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/transport.d.ts +59 -0
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +1 -0
- package/dist/runtime/transport.js.map +1 -1
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +0 -5
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +212 -169
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +50 -58
- package/dist/utils/cache.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 +53 -2
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/python.d.ts.map +1 -1
- package/dist/utils/python.js +28 -17
- package/dist/utils/python.js.map +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +22 -16
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -1
- package/runtime/__pycache__/_tywrap_member_fixtures.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/tywrap_bridge_core.py +55 -3
- package/src/core/annotation-parser.ts +23 -18
- package/src/core/discovery.ts +70 -54
- package/src/core/generator.ts +50 -11
- package/src/core/validation.ts +84 -48
- package/src/dev.ts +237 -153
- package/src/runtime/base-bridge.ts +106 -0
- package/src/runtime/bridge-codec.ts +58 -70
- package/src/runtime/http-transport.ts +21 -1
- package/src/runtime/http.ts +7 -51
- package/src/runtime/index.ts +1 -0
- package/src/runtime/node.ts +17 -52
- 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 +56 -32
- package/src/runtime/subprocess-transport.ts +89 -40
- package/src/runtime/transport.ts +71 -0
- package/src/types/index.ts +37 -0
- package/src/tywrap.ts +274 -187
- package/src/utils/cache.ts +59 -61
- package/src/utils/codec.ts +61 -4
- package/src/utils/python.ts +33 -20
- package/src/utils/runtime.ts +24 -13
- 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();
|
|
@@ -52,30 +53,6 @@ export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<Tywr
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
export default tywrap;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Append minimal tsd tests for a generated module (optional dev aid)
|
|
59
|
-
*/
|
|
60
|
-
export async function emitTypeTestsForModule(
|
|
61
|
-
moduleName: string,
|
|
62
|
-
outDir = 'test-d/generated'
|
|
63
|
-
): Promise<void> {
|
|
64
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
65
|
-
const { join } = await import('path');
|
|
66
|
-
try {
|
|
67
|
-
await mkdir(outDir, { recursive: true });
|
|
68
|
-
} catch {}
|
|
69
|
-
const filePath = join(outDir, `${moduleName}.test-d.ts`);
|
|
70
|
-
const content = `import { expectType } from 'tsd';
|
|
71
|
-
import * as mod from '../../generated/${moduleName}.generated.ts';
|
|
72
|
-
|
|
73
|
-
// Opportunistic: ensure module namespace is an object
|
|
74
|
-
expectType<Record<string, unknown>>(mod);
|
|
75
|
-
`;
|
|
76
|
-
await writeFile(filePath, content, 'utf-8');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
56
|
export interface GenerateRunOptions {
|
|
80
57
|
/**
|
|
81
58
|
* If true, do not write files; instead compare generated output to what's on disk.
|
|
@@ -114,6 +91,195 @@ async function safeReadFile(path: string): Promise<string | null> {
|
|
|
114
91
|
}
|
|
115
92
|
}
|
|
116
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
|
+
|
|
117
283
|
/**
|
|
118
284
|
* Generate TypeScript wrappers for configured Python modules.
|
|
119
285
|
* Minimal MVP implementation: resolves module file, analyzes, generates TS, writes to output.dir
|
|
@@ -131,7 +297,6 @@ export async function generate(
|
|
|
131
297
|
const failures: GenerateFailure[] = [];
|
|
132
298
|
const outputDir = resolvedOptions.output.dir;
|
|
133
299
|
const caching = resolvedOptions.performance.caching;
|
|
134
|
-
const cacheDir = '.tywrap/cache';
|
|
135
300
|
const pythonPath = await resolvePythonExecutable({
|
|
136
301
|
pythonPath: resolvedOptions.runtime?.node?.pythonPath,
|
|
137
302
|
virtualEnv: resolvedOptions.runtime?.node?.virtualEnv,
|
|
@@ -155,29 +320,14 @@ export async function generate(
|
|
|
155
320
|
unknownTypeNamesCollector = new Map();
|
|
156
321
|
// Resolve module IR via the Python extractor (with optional cache)
|
|
157
322
|
const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (!ir) {
|
|
169
|
-
const fetchResult = await fetchPythonIr(moduleKey, pythonPath, {
|
|
170
|
-
timeoutMs: resolvedOptions.runtime?.node?.timeout,
|
|
171
|
-
pythonImportPath: resolvedOptions.pythonImportPath,
|
|
172
|
-
});
|
|
173
|
-
ir = fetchResult.ir;
|
|
174
|
-
irError = fetchResult.error;
|
|
175
|
-
if (ir && caching && fsUtils.isAvailable() && !checkMode) {
|
|
176
|
-
try {
|
|
177
|
-
await fsUtils.writeFile(pathUtils.join(cacheDir, cacheKey), JSON.stringify(ir));
|
|
178
|
-
} catch {}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
323
|
+
const { ir, error: irError } = await fetchAndCacheIr(
|
|
324
|
+
moduleKey,
|
|
325
|
+
resolvedOptions,
|
|
326
|
+
pythonPath,
|
|
327
|
+
cacheKey,
|
|
328
|
+
caching,
|
|
329
|
+
checkMode
|
|
330
|
+
);
|
|
181
331
|
if (!ir) {
|
|
182
332
|
const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
|
|
183
333
|
warnings.push(message);
|
|
@@ -192,76 +342,7 @@ export async function generate(
|
|
|
192
342
|
const moduleModel = transformIrToTsModel(ir);
|
|
193
343
|
|
|
194
344
|
// Apply module-level export filtering (functions/classes + excludes).
|
|
195
|
-
|
|
196
|
-
const builtInDefaultExcludes = new Set([
|
|
197
|
-
'dataclass',
|
|
198
|
-
'property',
|
|
199
|
-
'staticmethod',
|
|
200
|
-
'classmethod',
|
|
201
|
-
'abstractmethod',
|
|
202
|
-
'cached_property',
|
|
203
|
-
]);
|
|
204
|
-
|
|
205
|
-
const excludeExact = new Set((moduleConfig.exclude ?? []).map(String));
|
|
206
|
-
const excludeRegexes: RegExp[] = [];
|
|
207
|
-
for (const pattern of moduleConfig.excludePatterns ?? []) {
|
|
208
|
-
try {
|
|
209
|
-
excludeRegexes.push(new RegExp(String(pattern)));
|
|
210
|
-
} catch (err: unknown) {
|
|
211
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
212
|
-
warnings.push(
|
|
213
|
-
`Module ${moduleKey}: invalid excludePatterns regex "${String(pattern)}": ${message}`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const shouldExclude = (name: string, applyBuiltInDefaults: boolean): boolean => {
|
|
219
|
-
if (excludeExact.has(name)) {
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
if (excludeRegexes.some(r => r.test(name))) {
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
if (applyBuiltInDefaults && builtInDefaultExcludes.has(name)) {
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
return false;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
if (Array.isArray(moduleConfig.functions)) {
|
|
232
|
-
const allow = new Set(moduleConfig.functions.map(String));
|
|
233
|
-
for (const requested of allow) {
|
|
234
|
-
if (!moduleModel.functions.some(f => f.name === requested)) {
|
|
235
|
-
warnings.push(
|
|
236
|
-
`Module ${moduleKey}: configured function "${requested}" not found in IR`
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
moduleModel.functions = moduleModel.functions.filter(
|
|
241
|
-
f => allow.has(f.name) && !shouldExclude(f.name, false)
|
|
242
|
-
);
|
|
243
|
-
} else {
|
|
244
|
-
moduleModel.functions = moduleModel.functions.filter(f => !shouldExclude(f.name, true));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (Array.isArray(moduleConfig.classes)) {
|
|
248
|
-
const allow = new Set(moduleConfig.classes.map(String));
|
|
249
|
-
for (const requested of allow) {
|
|
250
|
-
if (!moduleModel.classes.some(c => c.name === requested)) {
|
|
251
|
-
warnings.push(`Module ${moduleKey}: configured class "${requested}" not found in IR`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
moduleModel.classes = moduleModel.classes.filter(
|
|
255
|
-
c => allow.has(c.name) && !shouldExclude(c.name, false)
|
|
256
|
-
);
|
|
257
|
-
} else {
|
|
258
|
-
moduleModel.classes = moduleModel.classes.filter(c => !shouldExclude(c.name, true));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
moduleModel.typeAliases = (moduleModel.typeAliases ?? []).filter(
|
|
262
|
-
alias => !shouldExclude(alias.name, false)
|
|
263
|
-
);
|
|
264
|
-
}
|
|
345
|
+
filterModuleExports(moduleModel, moduleConfig, moduleKey, warnings);
|
|
265
346
|
|
|
266
347
|
// Generate module code
|
|
267
348
|
const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
|
|
@@ -288,51 +369,10 @@ export async function generate(
|
|
|
288
369
|
});
|
|
289
370
|
}
|
|
290
371
|
|
|
291
|
-
|
|
292
|
-
for (const file of filesToEmit) {
|
|
293
|
-
const existing = await safeReadFile(file.path);
|
|
294
|
-
if (existing === null) {
|
|
295
|
-
outOfDate.push(file.path);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (normalizeForComparison(existing) !== normalizeForComparison(file.content)) {
|
|
299
|
-
outOfDate.push(file.path);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} else {
|
|
303
|
-
for (const file of filesToEmit) {
|
|
304
|
-
await fsUtils.writeFile(file.path, file.content);
|
|
305
|
-
written.push(file.path);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
372
|
+
await emitOrCompareFiles(filesToEmit, checkMode, written, outOfDate);
|
|
308
373
|
|
|
309
374
|
// Emit warning summary of unknown typing constructs (best-effort)
|
|
310
|
-
|
|
311
|
-
const entries = Array.from(unknownTypeNamesCollector.entries()).sort((a, b) => b[1] - a[1]);
|
|
312
|
-
const unkList = entries
|
|
313
|
-
.slice(0, 25)
|
|
314
|
-
.map(([n, c]) => `${n}:${c}`)
|
|
315
|
-
.join(', ');
|
|
316
|
-
warnings.push(
|
|
317
|
-
`Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
|
|
318
|
-
);
|
|
319
|
-
// Write JSON report
|
|
320
|
-
if (!checkMode) {
|
|
321
|
-
try {
|
|
322
|
-
const reportsDir = pathUtils.join('.tywrap', 'reports');
|
|
323
|
-
await fsUtils.writeFile(
|
|
324
|
-
pathUtils.join(reportsDir, `${baseName}.json`),
|
|
325
|
-
JSON.stringify({
|
|
326
|
-
module: baseName,
|
|
327
|
-
unknowns: Object.fromEntries(entries),
|
|
328
|
-
generatedAt: new Date().toISOString(),
|
|
329
|
-
})
|
|
330
|
-
);
|
|
331
|
-
} catch {
|
|
332
|
-
// ignore
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
375
|
+
await reportUnknownTypes(baseName, unknownTypeNamesCollector, checkMode, warnings);
|
|
336
376
|
}
|
|
337
377
|
|
|
338
378
|
if (checkMode) {
|
|
@@ -342,6 +382,40 @@ export async function generate(
|
|
|
342
382
|
return { written, warnings, failures };
|
|
343
383
|
}
|
|
344
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
|
+
|
|
345
419
|
/**
|
|
346
420
|
* Invoke the Python IR CLI to get JSON IR for a module.
|
|
347
421
|
*/
|
|
@@ -363,24 +437,27 @@ async function fetchPythonIr(
|
|
|
363
437
|
.filter(Boolean)
|
|
364
438
|
.join(delimiter);
|
|
365
439
|
const env = mergedPyPath ? { PYTHONPATH: mergedPyPath } : undefined;
|
|
440
|
+
const execOptions = { timeoutMs: options.timeoutMs, env };
|
|
366
441
|
try {
|
|
367
|
-
const
|
|
442
|
+
const primary = await execAndParseIr(
|
|
368
443
|
pythonPath,
|
|
369
|
-
[
|
|
370
|
-
|
|
444
|
+
[
|
|
445
|
+
'-m',
|
|
446
|
+
'tywrap_ir',
|
|
447
|
+
'--module',
|
|
448
|
+
moduleName,
|
|
449
|
+
'--ir-version',
|
|
450
|
+
TYWRAP_IR_VERSION,
|
|
451
|
+
'--no-pretty',
|
|
452
|
+
],
|
|
453
|
+
execOptions,
|
|
454
|
+
'tywrap_ir output'
|
|
371
455
|
);
|
|
372
|
-
if (result.code === 0) {
|
|
373
|
-
|
|
374
|
-
return { ir: JSON.parse(result.stdout) };
|
|
375
|
-
} catch {
|
|
376
|
-
return {
|
|
377
|
-
ir: null,
|
|
378
|
-
error: `Failed to parse tywrap_ir output. stderr: ${result.stderr.trim() || 'empty'}`,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
456
|
+
if (primary.result.code === 0) {
|
|
457
|
+
return { ir: primary.ir, error: primary.error };
|
|
381
458
|
}
|
|
382
459
|
|
|
383
|
-
const stderrText = result.stderr.trim();
|
|
460
|
+
const stderrText = primary.result.stderr.trim();
|
|
384
461
|
const isTywrapIrMissing =
|
|
385
462
|
stderrText.includes('No module named') && stderrText.includes('tywrap_ir');
|
|
386
463
|
if (!isTywrapIrMissing) {
|
|
@@ -405,25 +482,19 @@ async function fetchPythonIr(
|
|
|
405
482
|
};
|
|
406
483
|
}
|
|
407
484
|
|
|
408
|
-
const fallback = await
|
|
485
|
+
const fallback = await execAndParseIr(
|
|
409
486
|
pythonPath,
|
|
410
487
|
[localMain, '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
|
|
411
|
-
|
|
488
|
+
execOptions,
|
|
489
|
+
'tywrap_ir fallback output'
|
|
412
490
|
);
|
|
413
|
-
if (fallback.code !== 0) {
|
|
491
|
+
if (fallback.result.code !== 0) {
|
|
414
492
|
return {
|
|
415
493
|
ir: null,
|
|
416
|
-
error: `tywrap_ir failed. stderr: ${fallback.stderr.trim() || stderrText || 'empty'}`,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
try {
|
|
420
|
-
return { ir: JSON.parse(fallback.stdout) };
|
|
421
|
-
} catch {
|
|
422
|
-
return {
|
|
423
|
-
ir: null,
|
|
424
|
-
error: `Failed to parse tywrap_ir fallback output. stderr: ${fallback.stderr.trim() || 'empty'}`,
|
|
494
|
+
error: `tywrap_ir failed. stderr: ${fallback.result.stderr.trim() || stderrText || 'empty'}`,
|
|
425
495
|
};
|
|
426
496
|
}
|
|
497
|
+
return { ir: fallback.ir, error: fallback.error };
|
|
427
498
|
} catch (err) {
|
|
428
499
|
return { ir: null, error: err instanceof Error ? err.message : String(err) };
|
|
429
500
|
}
|
|
@@ -507,6 +578,9 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
507
578
|
keywordOnly: p.kind === 'KEYWORD_ONLY',
|
|
508
579
|
});
|
|
509
580
|
|
|
581
|
+
const mapMethodKind = (value: unknown): PythonFunction['methodKind'] =>
|
|
582
|
+
value === 'class' || value === 'static' ? value : 'instance';
|
|
583
|
+
|
|
510
584
|
const mapFunc = (
|
|
511
585
|
f: Record<string, unknown>,
|
|
512
586
|
inheritedTypeParameters: readonly PythonGenericParameter[] = []
|
|
@@ -536,6 +610,7 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
536
610
|
mapParam((v ?? {}) as Record<string, unknown>, annotationTypeParameters)
|
|
537
611
|
)
|
|
538
612
|
: [],
|
|
613
|
+
methodKind: mapMethodKind(f.method_kind),
|
|
539
614
|
};
|
|
540
615
|
};
|
|
541
616
|
|
|
@@ -562,6 +637,18 @@ function transformIrToTsModel(ir: unknown): TSPythonModule {
|
|
|
562
637
|
};
|
|
563
638
|
})
|
|
564
639
|
: [],
|
|
640
|
+
accessors: Array.isArray(c.accessors)
|
|
641
|
+
? (c.accessors as unknown[]).map(v => {
|
|
642
|
+
const a = (v ?? {}) as Record<string, unknown>;
|
|
643
|
+
return {
|
|
644
|
+
name: String(a.name ?? ''),
|
|
645
|
+
type: parseType(a.returns, classTypeParameters),
|
|
646
|
+
docstring: (a.docstring as string | undefined) ?? undefined,
|
|
647
|
+
readOnly: typeof a.read_only === 'boolean' ? a.read_only : undefined,
|
|
648
|
+
isCached: Boolean(a.is_cached),
|
|
649
|
+
};
|
|
650
|
+
})
|
|
651
|
+
: [],
|
|
565
652
|
docstring: (c.docstring as string | undefined) ?? undefined,
|
|
566
653
|
decorators: (c.typed_dict as boolean) ? ['__typed_dict__'] : [],
|
|
567
654
|
kind: (c.typed_dict as boolean)
|
package/src/utils/cache.ts
CHANGED
|
@@ -149,75 +149,73 @@ export class ArtifactCache {
|
|
|
149
149
|
hash.update(prefix);
|
|
150
150
|
hash.update('\0');
|
|
151
151
|
|
|
152
|
-
// Add inputs
|
|
152
|
+
// Add inputs. Each input contributes a type tag, an optional payload, and a
|
|
153
|
+
// boundary separator so that "1" vs 1 and ["a","bc"] vs ["ab","c"] never collide.
|
|
153
154
|
for (const input of inputs) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
hash.update(
|
|
158
|
-
hash.update('\0');
|
|
159
|
-
continue;
|
|
155
|
+
const { tag, payload } = ArtifactCache.encodeKeyInput(input);
|
|
156
|
+
hash.update(tag);
|
|
157
|
+
if (payload !== undefined) {
|
|
158
|
+
hash.update(payload);
|
|
160
159
|
}
|
|
161
|
-
if (input === null) {
|
|
162
|
-
hash.update('null:');
|
|
163
|
-
hash.update('\0');
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (typeof input === 'string') {
|
|
167
|
-
hash.update('str:');
|
|
168
|
-
hash.update(input);
|
|
169
|
-
hash.update('\0');
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (typeof input === 'number') {
|
|
173
|
-
hash.update('num:');
|
|
174
|
-
hash.update(String(input));
|
|
175
|
-
hash.update('\0');
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (typeof input === 'boolean') {
|
|
179
|
-
hash.update('bool:');
|
|
180
|
-
hash.update(input ? '1' : '0');
|
|
181
|
-
hash.update('\0');
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (typeof input === 'bigint') {
|
|
185
|
-
hash.update('bigint:');
|
|
186
|
-
hash.update(input.toString());
|
|
187
|
-
hash.update('\0');
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (Buffer.isBuffer(input)) {
|
|
191
|
-
hash.update('buf:');
|
|
192
|
-
hash.update(input);
|
|
193
|
-
hash.update('\0');
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (typeof input === 'symbol') {
|
|
198
|
-
hash.update('sym:');
|
|
199
|
-
hash.update(input.toString());
|
|
200
|
-
hash.update('\0');
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
hash.update('json:');
|
|
205
|
-
let json: string;
|
|
206
|
-
try {
|
|
207
|
-
json =
|
|
208
|
-
JSON.stringify(input, (_key, value) =>
|
|
209
|
-
typeof value === 'symbol' ? value.toString() : value
|
|
210
|
-
) ?? 'undefined';
|
|
211
|
-
} catch {
|
|
212
|
-
json = String(input);
|
|
213
|
-
}
|
|
214
|
-
hash.update(json);
|
|
215
160
|
hash.update('\0');
|
|
216
161
|
}
|
|
217
162
|
|
|
218
163
|
return hash.digest('hex').substring(0, 16); // Use first 16 chars for readability
|
|
219
164
|
}
|
|
220
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Encode a single key input as a type tag plus optional payload.
|
|
168
|
+
* Why: disambiguate types so "1" and 1 don't collide, and keep input boundaries so
|
|
169
|
+
* ["a", "bc"] doesn't collide with ["ab", "c"].
|
|
170
|
+
*/
|
|
171
|
+
private static encodeKeyInput(input: unknown): {
|
|
172
|
+
tag: string;
|
|
173
|
+
payload?: string | Buffer;
|
|
174
|
+
} {
|
|
175
|
+
if (input === undefined) {
|
|
176
|
+
return { tag: 'undef:' };
|
|
177
|
+
}
|
|
178
|
+
if (input === null) {
|
|
179
|
+
return { tag: 'null:' };
|
|
180
|
+
}
|
|
181
|
+
if (typeof input === 'string') {
|
|
182
|
+
return { tag: 'str:', payload: input };
|
|
183
|
+
}
|
|
184
|
+
if (typeof input === 'number') {
|
|
185
|
+
return { tag: 'num:', payload: String(input) };
|
|
186
|
+
}
|
|
187
|
+
if (typeof input === 'boolean') {
|
|
188
|
+
return { tag: 'bool:', payload: input ? '1' : '0' };
|
|
189
|
+
}
|
|
190
|
+
if (typeof input === 'bigint') {
|
|
191
|
+
return { tag: 'bigint:', payload: input.toString() };
|
|
192
|
+
}
|
|
193
|
+
if (Buffer.isBuffer(input)) {
|
|
194
|
+
return { tag: 'buf:', payload: input };
|
|
195
|
+
}
|
|
196
|
+
if (typeof input === 'symbol') {
|
|
197
|
+
return { tag: 'sym:', payload: input.toString() };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { tag: 'json:', payload: ArtifactCache.stringifyKeyInput(input) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Stringify an arbitrary key input for hashing, normalizing symbols and
|
|
205
|
+
* falling back to String() when JSON serialization fails.
|
|
206
|
+
*/
|
|
207
|
+
private static stringifyKeyInput(input: unknown): string {
|
|
208
|
+
try {
|
|
209
|
+
return (
|
|
210
|
+
JSON.stringify(input, (_key, value) =>
|
|
211
|
+
typeof value === 'symbol' ? value.toString() : value
|
|
212
|
+
) ?? 'undefined'
|
|
213
|
+
);
|
|
214
|
+
} catch {
|
|
215
|
+
return String(input);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
221
219
|
/**
|
|
222
220
|
* Get entry from cache with performance tracking
|
|
223
221
|
*/
|