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