tywrap 0.2.1 → 0.4.0

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