tywrap 0.2.1 → 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 (138) 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 +7 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/bridge-core.d.ts +1 -0
  33. package/dist/runtime/bridge-core.d.ts.map +1 -1
  34. package/dist/runtime/bridge-core.js +52 -17
  35. package/dist/runtime/bridge-core.js.map +1 -1
  36. package/dist/runtime/bridge-protocol.d.ts +17 -0
  37. package/dist/runtime/bridge-protocol.d.ts.map +1 -1
  38. package/dist/runtime/bridge-protocol.js +97 -0
  39. package/dist/runtime/bridge-protocol.js.map +1 -1
  40. package/dist/runtime/errors.d.ts +10 -0
  41. package/dist/runtime/errors.d.ts.map +1 -1
  42. package/dist/runtime/errors.js +9 -0
  43. package/dist/runtime/errors.js.map +1 -1
  44. package/dist/runtime/http-io.d.ts.map +1 -1
  45. package/dist/runtime/http-io.js.map +1 -1
  46. package/dist/runtime/http.d.ts +1 -0
  47. package/dist/runtime/http.d.ts.map +1 -1
  48. package/dist/runtime/http.js +6 -0
  49. package/dist/runtime/http.js.map +1 -1
  50. package/dist/runtime/node.d.ts.map +1 -1
  51. package/dist/runtime/node.js +170 -32
  52. package/dist/runtime/node.js.map +1 -1
  53. package/dist/runtime/optimized-node.d.ts +1 -1
  54. package/dist/runtime/optimized-node.d.ts.map +1 -1
  55. package/dist/runtime/optimized-node.js +1 -1
  56. package/dist/runtime/optimized-node.js.map +1 -1
  57. package/dist/runtime/pooled-transport.d.ts +4 -0
  58. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  59. package/dist/runtime/pooled-transport.js +2 -0
  60. package/dist/runtime/pooled-transport.js.map +1 -1
  61. package/dist/runtime/process-io.d.ts +5 -0
  62. package/dist/runtime/process-io.d.ts.map +1 -1
  63. package/dist/runtime/process-io.js +64 -30
  64. package/dist/runtime/process-io.js.map +1 -1
  65. package/dist/runtime/pyodide-io.d.ts +4 -5
  66. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  67. package/dist/runtime/pyodide-io.js +99 -37
  68. package/dist/runtime/pyodide-io.js.map +1 -1
  69. package/dist/runtime/safe-codec.d.ts +25 -3
  70. package/dist/runtime/safe-codec.d.ts.map +1 -1
  71. package/dist/runtime/safe-codec.js +231 -57
  72. package/dist/runtime/safe-codec.js.map +1 -1
  73. package/dist/runtime/transport.d.ts +6 -1
  74. package/dist/runtime/transport.d.ts.map +1 -1
  75. package/dist/runtime/transport.js.map +1 -1
  76. package/dist/runtime/validators.d.ts +0 -11
  77. package/dist/runtime/validators.d.ts.map +1 -1
  78. package/dist/runtime/validators.js +13 -3
  79. package/dist/runtime/validators.js.map +1 -1
  80. package/dist/runtime/worker-pool.d.ts +12 -0
  81. package/dist/runtime/worker-pool.d.ts.map +1 -1
  82. package/dist/runtime/worker-pool.js +53 -5
  83. package/dist/runtime/worker-pool.js.map +1 -1
  84. package/dist/types/index.d.ts +51 -1
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/dist/tywrap.d.ts.map +1 -1
  87. package/dist/tywrap.js +227 -328
  88. package/dist/tywrap.js.map +1 -1
  89. package/dist/utils/cache.d.ts.map +1 -1
  90. package/dist/utils/cache.js +53 -5
  91. package/dist/utils/cache.js.map +1 -1
  92. package/dist/utils/codec.d.ts.map +1 -1
  93. package/dist/utils/codec.js +10 -16
  94. package/dist/utils/codec.js.map +1 -1
  95. package/dist/utils/ir-cache.d.ts +28 -0
  96. package/dist/utils/ir-cache.d.ts.map +1 -0
  97. package/dist/utils/ir-cache.js +29 -0
  98. package/dist/utils/ir-cache.js.map +1 -0
  99. package/dist/utils/parallel-processor.d.ts +1 -1
  100. package/dist/utils/parallel-processor.d.ts.map +1 -1
  101. package/dist/utils/parallel-processor.js +111 -40
  102. package/dist/utils/parallel-processor.js.map +1 -1
  103. package/dist/utils/runtime.d.ts +5 -1
  104. package/dist/utils/runtime.d.ts.map +1 -1
  105. package/dist/utils/runtime.js +112 -18
  106. package/dist/utils/runtime.js.map +1 -1
  107. package/package.json +40 -18
  108. package/runtime/python_bridge.py +78 -12
  109. package/src/cli.ts +61 -2
  110. package/src/config/index.ts +63 -17
  111. package/src/core/analyzer.ts +52 -15
  112. package/src/core/annotation-parser.ts +500 -0
  113. package/src/core/discovery.ts +26 -12
  114. package/src/core/generator.ts +956 -116
  115. package/src/core/mapper.ts +158 -51
  116. package/src/index.ts +34 -5
  117. package/src/runtime/bridge-core.ts +58 -18
  118. package/src/runtime/bridge-protocol.ts +164 -0
  119. package/src/runtime/errors.ts +14 -0
  120. package/src/runtime/http-io.ts +2 -1
  121. package/src/runtime/http.ts +7 -0
  122. package/src/runtime/node.ts +264 -44
  123. package/src/runtime/optimized-node.ts +4 -1
  124. package/src/runtime/pooled-transport.ts +12 -1
  125. package/src/runtime/process-io.ts +83 -55
  126. package/src/runtime/pyodide-io.ts +110 -44
  127. package/src/runtime/safe-codec.ts +290 -82
  128. package/src/runtime/transport.ts +6 -1
  129. package/src/runtime/validators.ts +14 -3
  130. package/src/runtime/worker-pool.ts +67 -6
  131. package/src/types/global.d.ts +11 -1
  132. package/src/types/index.ts +63 -0
  133. package/src/tywrap.ts +279 -358
  134. package/src/utils/cache.ts +59 -5
  135. package/src/utils/codec.ts +11 -15
  136. package/src/utils/ir-cache.ts +51 -0
  137. package/src/utils/parallel-processor.ts +119 -43
  138. package/src/utils/runtime.ts +129 -18
@@ -89,26 +89,243 @@ export class CodeGenerator {
89
89
  return unicodeMap.get(char) ?? char.charCodeAt(0).toString(16);
90
90
  });
91
91
  }
92
+ getTypeParameters(typeParameters) {
93
+ return typeParameters ?? [];
94
+ }
95
+ buildGenericRenderContext(typeParameters, types, currentModule) {
96
+ const callableParamSpecs = new Set();
97
+ types.forEach(type => this.collectCallableParamSpecs(type, callableParamSpecs));
98
+ const emitted = [];
99
+ const emittedNames = new Set();
100
+ const emittedParamSpecs = new Set();
101
+ typeParameters.forEach(param => {
102
+ if (param.kind === 'typevar' &&
103
+ !param.bound &&
104
+ !(param.constraints && param.constraints.length > 0) &&
105
+ (!param.variance || param.variance === 'invariant')) {
106
+ emitted.push({ name: param.name, declaration: param.name });
107
+ emittedNames.add(param.name);
108
+ return;
109
+ }
110
+ if (param.kind === 'paramspec' && callableParamSpecs.has(param.name)) {
111
+ emitted.push({ name: param.name, declaration: `${param.name} extends unknown[]` });
112
+ emittedNames.add(param.name);
113
+ emittedParamSpecs.add(param.name);
114
+ }
115
+ });
116
+ return {
117
+ currentModule,
118
+ declaration: emitted.length > 0 ? `<${emitted.map(param => param.declaration).join(', ')}>` : '',
119
+ typeArguments: emitted.length > 0 ? `<${emitted.map(param => param.name).join(', ')}>` : '',
120
+ emittedNames,
121
+ emittedParamSpecs,
122
+ };
123
+ }
124
+ mergeGenericRenderContexts(outer, inner) {
125
+ return {
126
+ currentModule: inner.currentModule ?? outer.currentModule,
127
+ declaration: inner.declaration,
128
+ typeArguments: inner.typeArguments,
129
+ emittedNames: new Set([...outer.emittedNames, ...inner.emittedNames]),
130
+ emittedParamSpecs: new Set([...outer.emittedParamSpecs, ...inner.emittedParamSpecs]),
131
+ };
132
+ }
133
+ collectCallableParamSpecs(type, out) {
134
+ switch (type.kind) {
135
+ case 'collection':
136
+ type.itemTypes.forEach(item => this.collectCallableParamSpecs(item, out));
137
+ break;
138
+ case 'paramspec':
139
+ out.add(type.name);
140
+ break;
141
+ case 'union':
142
+ type.types.forEach(item => this.collectCallableParamSpecs(item, out));
143
+ break;
144
+ case 'optional':
145
+ this.collectCallableParamSpecs(type.type, out);
146
+ break;
147
+ case 'generic':
148
+ type.typeArgs.forEach(item => this.collectCallableParamSpecs(item, out));
149
+ break;
150
+ case 'callable':
151
+ if (type.parameterSpec?.kind === 'paramspec') {
152
+ out.add(type.parameterSpec.name);
153
+ }
154
+ type.parameters.forEach(item => this.collectCallableParamSpecs(item, out));
155
+ this.collectCallableParamSpecs(type.returnType, out);
156
+ break;
157
+ case 'annotated':
158
+ this.collectCallableParamSpecs(type.base, out);
159
+ break;
160
+ case 'final':
161
+ case 'classvar':
162
+ this.collectCallableParamSpecs(type.type, out);
163
+ break;
164
+ case 'unpack':
165
+ this.collectCallableParamSpecs(type.type, out);
166
+ break;
167
+ default:
168
+ break;
169
+ }
170
+ }
171
+ sanitizeType(type, ctx) {
172
+ const unknownType = () => ({ kind: 'custom', name: 'Any', module: 'typing' });
173
+ switch (type.kind) {
174
+ case 'primitive':
175
+ case 'literal':
176
+ return type;
177
+ case 'custom':
178
+ return type;
179
+ case 'collection':
180
+ return {
181
+ ...type,
182
+ itemTypes: type.itemTypes.map(item => this.sanitizeType(item, ctx)),
183
+ };
184
+ case 'union':
185
+ return {
186
+ ...type,
187
+ types: type.types.map(item => this.sanitizeType(item, ctx)),
188
+ };
189
+ case 'optional':
190
+ return { ...type, type: this.sanitizeType(type.type, ctx) };
191
+ case 'generic':
192
+ return {
193
+ ...type,
194
+ module: type.module === ctx.currentModule ? undefined : type.module,
195
+ typeArgs: type.typeArgs.map(item => this.sanitizeType(item, ctx)),
196
+ };
197
+ case 'callable':
198
+ if (type.parameterSpec && !ctx.emittedParamSpecs.has(type.parameterSpec.name)) {
199
+ return {
200
+ ...type,
201
+ parameters: [{ kind: 'custom', name: '...' }],
202
+ parameterSpec: undefined,
203
+ returnType: this.sanitizeType(type.returnType, ctx),
204
+ };
205
+ }
206
+ return {
207
+ ...type,
208
+ parameters: type.parameters.map(item => this.sanitizeType(item, ctx)),
209
+ parameterSpec: type.parameterSpec && ctx.emittedParamSpecs.has(type.parameterSpec.name)
210
+ ? type.parameterSpec
211
+ : undefined,
212
+ returnType: this.sanitizeType(type.returnType, ctx),
213
+ };
214
+ case 'annotated':
215
+ return { ...type, base: this.sanitizeType(type.base, ctx) };
216
+ case 'typevar':
217
+ case 'paramspec':
218
+ return ctx.emittedNames.has(type.name) ? type : unknownType();
219
+ case 'paramspec_args':
220
+ return {
221
+ kind: 'collection',
222
+ name: 'list',
223
+ itemTypes: [unknownType()],
224
+ };
225
+ case 'paramspec_kwargs':
226
+ return {
227
+ kind: 'collection',
228
+ name: 'dict',
229
+ itemTypes: [{ kind: 'primitive', name: 'str' }, unknownType()],
230
+ };
231
+ case 'typevartuple':
232
+ return unknownType();
233
+ case 'unpack':
234
+ return unknownType();
235
+ case 'final':
236
+ return { ...type, type: this.sanitizeType(type.type, ctx) };
237
+ case 'classvar':
238
+ return { ...type, type: this.sanitizeType(type.type, ctx) };
239
+ }
240
+ }
241
+ typeToTsFromPython(type, ctx, mappingContext) {
242
+ return this.typeToTs(this.mapper.mapPythonType(this.sanitizeType(type, ctx), mappingContext));
243
+ }
244
+ renderLooksLikeKwargsExpr(valueExpr, options) {
245
+ const base = `typeof ${valueExpr} === 'object' && ${valueExpr} !== null && !globalThis.Array.isArray(${valueExpr}) && (Object.getPrototypeOf(${valueExpr}) === Object.prototype || Object.getPrototypeOf(${valueExpr}) === null)`;
246
+ const keyCheck = (() => {
247
+ if (options.requiredKwOnlyNames.length > 0) {
248
+ return options.requiredKwOnlyNames
249
+ .map(k => `Object.prototype.hasOwnProperty.call(${valueExpr}, ${JSON.stringify(k)})`)
250
+ .join(' && ');
251
+ }
252
+ if (options.hasVarKwArgs) {
253
+ // With **kwargs, any plain object could be kwargs.
254
+ return 'true';
255
+ }
256
+ if (options.keywordOnlyNames.length > 0) {
257
+ return options.keywordOnlyNames
258
+ .map(k => `Object.prototype.hasOwnProperty.call(${valueExpr}, ${JSON.stringify(k)})`)
259
+ .join(' || ');
260
+ }
261
+ return 'false';
262
+ })();
263
+ return `(${base} && (${keyCheck}))`;
264
+ }
92
265
  generateFunctionWrapper(func, moduleName, annotatedJSDoc = false) {
93
266
  const jsdoc = this.generateJsDoc(func.docstring, annotatedJSDoc ? func.parameters.map(p => String(p.type)) : undefined);
94
267
  const filteredParams = func.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
95
- const renderParam = (p) => {
268
+ const keywordOnlyParams = filteredParams.filter(p => p.keywordOnly);
269
+ const positionalOnlyNames = filteredParams.filter(p => p.positionalOnly).map(p => p.name);
270
+ const hasVarKwArgs = filteredParams.some(p => p.kwArgs);
271
+ const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
272
+ const varArgsParam = filteredParams.find(p => p.varArgs);
273
+ const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
274
+ const positionalParams = filteredParams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
275
+ const genericContext = this.buildGenericRenderContext(this.getTypeParameters(func.typeParameters), [func.returnType, ...filteredParams.map(param => param.type)], moduleName);
276
+ const typeParamDecl = genericContext.declaration;
277
+ const tsTypeForValue = (p) => this.typeToTsFromPython(p.type, genericContext, 'value');
278
+ const kwargsType = (() => {
279
+ if (!needsKwargsParam) {
280
+ return '';
281
+ }
282
+ if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
283
+ return 'Record<string, unknown>';
284
+ }
285
+ const props = keywordOnlyParams
286
+ .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsTypeForValue(p)};`)
287
+ .join(' ');
288
+ const obj = `{ ${props} }`;
289
+ return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
290
+ })();
291
+ const renderPositionalParam = (p, forceRequired = false) => {
96
292
  const pname = this.escapeIdentifier(p.name);
97
- if (p.varArgs) {
293
+ const opt = !forceRequired && p.optional ? '?' : '';
294
+ return `${pname}${opt}: ${tsTypeForValue(p)}`;
295
+ };
296
+ const renderVarArgsParam = (forceRequired = false) => {
297
+ if (!varArgsParam) {
298
+ return null;
299
+ }
300
+ const pname = this.escapeIdentifier(varArgsParam.name);
301
+ if (!needsVarArgsArray) {
98
302
  return `...${pname}: unknown[]`;
99
303
  }
100
- if (p.kwArgs) {
101
- return `kwargs?: Record<string, unknown>`;
304
+ const opt = forceRequired ? '' : '?';
305
+ return `${pname}${opt}: unknown[]`;
306
+ };
307
+ const renderKwargsParam = (forceRequired = false) => {
308
+ if (!needsKwargsParam) {
309
+ return null;
102
310
  }
103
- return `${pname}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`;
311
+ const opt = forceRequired ? '' : '?';
312
+ return `kwargs${opt}: ${kwargsType}`;
104
313
  };
105
- const paramDecl = filteredParams.map(renderParam).join(', ');
106
- const positionalParams = filteredParams.filter(p => !p.kwArgs);
107
- const callArgs = positionalParams
108
- .map(p => (p.varArgs ? `...${this.escapeIdentifier(p.name)}` : this.escapeIdentifier(p.name)))
109
- .join(', ');
110
- const hasKwArgs = filteredParams.some(p => p.kwArgs);
111
- const returnType = this.typeToTs(this.mapper.mapPythonType(func.returnType, 'return'));
314
+ const implParams = [];
315
+ for (const p of positionalParams) {
316
+ implParams.push(renderPositionalParam(p));
317
+ }
318
+ const varArgsDecl = renderVarArgsParam(false);
319
+ if (varArgsDecl) {
320
+ implParams.push(varArgsDecl);
321
+ }
322
+ const kwargsDecl = needsKwargsParam ? `kwargs?: ${kwargsType}` : null;
323
+ if (kwargsDecl) {
324
+ implParams.push(kwargsDecl);
325
+ }
326
+ const paramDecl = implParams.join(', ');
327
+ const hasKwArgs = needsKwargsParam;
328
+ const returnType = this.typeToTsFromPython(func.returnType, genericContext, 'return');
112
329
  const fname = this.escapeIdentifier(func.name);
113
330
  const moduleId = moduleName ?? '__main__';
114
331
  // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
@@ -116,136 +333,540 @@ export class CodeGenerator {
116
333
  // `fn(a)`, `fn(a, b)`, ... all to typecheck. We emit a family of overloads that progressively
117
334
  // "drop" optional tail args, but also include the full positional signature (<= length) so a
118
335
  // call that supplies all args still matches an overload.
119
- const positional = filteredParams.filter(p => !p.varArgs && !p.kwArgs);
120
- const firstOptionalIndex = positional.findIndex(p => p.optional);
336
+ const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
337
+ const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
338
+ const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
121
339
  const overloads = [];
122
- if (firstOptionalIndex >= 0) {
123
- for (let i = firstOptionalIndex; i <= positional.length; i++) {
124
- const head = positional.slice(0, i);
125
- const rest = filteredParams.filter(p => p.varArgs || p.kwArgs);
126
- const sigParams = [...head, ...rest].map(renderParam).join(', ');
127
- overloads.push(`export function ${fname}(${sigParams}): Promise<${returnType}>;`);
340
+ if (requiredKwOnlyNames.length > 0) {
341
+ // Required keyword-only params must be represented with a required `kwargs` parameter.
342
+ // Avoid "required after optional" by emitting overloads where all preceding parameters are required.
343
+ const requiredPosCount = firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
344
+ for (let i = requiredPosCount; i <= positionalParams.length; i++) {
345
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
346
+ const rest = [];
347
+ const v = (() => {
348
+ if (!varArgsParam) {
349
+ return null;
350
+ }
351
+ const pname = this.escapeIdentifier(varArgsParam.name);
352
+ if (!needsVarArgsArray) {
353
+ return `...${pname}: unknown[]`;
354
+ }
355
+ // In overloads where `kwargs` is required, keep the `args` surrogate parameter required,
356
+ // but allow `undefined` as a placeholder so callers can omit varargs while still passing kwargs.
357
+ return `${pname}: unknown[] | undefined`;
358
+ })();
359
+ if (v) {
360
+ rest.push(v);
361
+ }
362
+ const k = renderKwargsParam(true);
363
+ if (k) {
364
+ rest.push(k);
365
+ }
366
+ overloads.push(`export function ${fname}${typeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${returnType}>;`);
367
+ if (varArgsParam && needsVarArgsArray) {
368
+ // Also allow callers to omit the varargs surrogate parameter entirely (i.e. `fn(kwargs)`).
369
+ overloads.push(`export function ${fname}${typeParamDecl}(${[...head, renderKwargsParam(true)].join(', ')}): Promise<${returnType}>;`);
370
+ }
371
+ }
372
+ }
373
+ else if (firstOptionalIndex >= 0 && !varArgsParam && !needsKwargsParam) {
374
+ for (let i = firstOptionalIndex; i <= positionalParams.length; i++) {
375
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p));
376
+ const rest = [];
377
+ const v = renderVarArgsParam(false);
378
+ if (v) {
379
+ rest.push(v);
380
+ }
381
+ const k = renderKwargsParam(false);
382
+ if (k) {
383
+ rest.push(k);
384
+ }
385
+ overloads.push(`export function ${fname}${typeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${returnType}>;`);
128
386
  }
129
387
  }
130
388
  const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
131
- const ts = `${jsdoc}${overloadDecl}export async function ${fname}(${paramDecl}): Promise<${returnType}> {
132
- return getRuntimeBridge().call('${moduleId}', '${func.name}', [${callArgs}]${hasKwArgs ? ', kwargs' : ''});
389
+ const guardLines = [];
390
+ if (hasKwArgs && positionalOnlyNames.length > 0) {
391
+ guardLines.push(` const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`);
392
+ guardLines.push(` for (const key of __positionalOnly) {`);
393
+ guardLines.push(` if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
394
+ guardLines.push(` throw new Error(\`${func.name} does not accept positional-only argument "\${key}" as a keyword argument\`);`);
395
+ guardLines.push(` }`);
396
+ guardLines.push(` }`);
397
+ }
398
+ if (hasKwArgs && requiredKwOnlyNames.length > 0) {
399
+ guardLines.push(` const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`);
400
+ guardLines.push(` const __missing: string[] = [];`);
401
+ guardLines.push(` for (const key of __requiredKwOnly) {`);
402
+ guardLines.push(` if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
403
+ guardLines.push(` __missing.push(key);`);
404
+ guardLines.push(` }`);
405
+ guardLines.push(` }`);
406
+ guardLines.push(` if (__missing.length > 0) {`);
407
+ guardLines.push(` throw new Error(\`Missing required keyword-only arguments for ${func.name}: \${__missing.join(', ')}\`);`);
408
+ guardLines.push(` }`);
409
+ }
410
+ const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
411
+ const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
412
+ const requiredPosCount = firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
413
+ const callPreludeLines = [];
414
+ if (hasKwArgs) {
415
+ callPreludeLines.push(` let __kwargs = kwargs;`);
416
+ }
417
+ const positionalArgExprs = positionalParams.map(p => this.escapeIdentifier(p.name));
418
+ callPreludeLines.push(` const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
419
+ if (requiredPosCount < positionalParams.length) {
420
+ callPreludeLines.push(` while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`);
421
+ callPreludeLines.push(` __args.pop();`);
422
+ callPreludeLines.push(` }`);
423
+ }
424
+ if (hasKwArgs && requiredPosCount < positionalParams.length) {
425
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr('__candidate', {
426
+ keywordOnlyNames,
427
+ requiredKwOnlyNames,
428
+ hasVarKwArgs,
429
+ });
430
+ callPreludeLines.push(` if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`);
431
+ callPreludeLines.push(` const __candidate = __args[__args.length - 1];`);
432
+ callPreludeLines.push(` if (${looksLikeKwargs}) {`);
433
+ callPreludeLines.push(` __kwargs = __candidate as any;`);
434
+ callPreludeLines.push(` __args.pop();`);
435
+ callPreludeLines.push(` }`);
436
+ callPreludeLines.push(` }`);
437
+ }
438
+ if (varArgsParam) {
439
+ const vname = this.escapeIdentifier(varArgsParam.name);
440
+ if (needsVarArgsArray) {
441
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr(vname, {
442
+ keywordOnlyNames,
443
+ requiredKwOnlyNames,
444
+ hasVarKwArgs,
445
+ });
446
+ callPreludeLines.push(` let __varargs: unknown[] = [];`);
447
+ callPreludeLines.push(` if (${vname} !== undefined) {`);
448
+ callPreludeLines.push(` if (globalThis.Array.isArray(${vname})) {`);
449
+ callPreludeLines.push(` __varargs = ${vname};`);
450
+ callPreludeLines.push(` } else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
451
+ callPreludeLines.push(` __kwargs = ${vname} as any;`);
452
+ callPreludeLines.push(` } else {`);
453
+ callPreludeLines.push(` throw new Error(\`${func.name} expected ${varArgsParam.name} to be an array\`);`);
454
+ callPreludeLines.push(` }`);
455
+ callPreludeLines.push(` }`);
456
+ callPreludeLines.push(` __args.push(...__varargs);`);
457
+ }
458
+ else {
459
+ callPreludeLines.push(` __args.push(...${vname});`);
460
+ }
461
+ }
462
+ const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
463
+ const ts = `${jsdoc}${overloadDecl}export async function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}> {
464
+ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.name}', __args${hasKwArgs ? ', __kwargs' : ''});
133
465
  }
134
466
  `;
135
- return this.wrap(ts, [func.name]);
467
+ const declarationBody = overloads.length > 0
468
+ ? overloadDecl
469
+ : `export function ${fname}${typeParamDecl}(${paramDecl}): Promise<${returnType}>;\n`;
470
+ return this.wrap(ts, `${jsdoc}${declarationBody}`, [func.name]);
136
471
  }
137
472
  generateClassWrapper(cls, moduleName, _annotatedJSDoc = false) {
138
473
  const jsdoc = this.generateJsDoc(cls.docstring);
139
- // Structural type aliases for special kinds
474
+ const classGenericContext = this.buildGenericRenderContext(this.getTypeParameters(cls.typeParameters), [
475
+ ...cls.properties.map(property => property.type),
476
+ ...cls.methods.flatMap(method => [
477
+ method.returnType,
478
+ ...method.parameters.map(p => p.type),
479
+ ]),
480
+ ], moduleName);
481
+ const classTypeParamDecl = classGenericContext.declaration;
482
+ const cname = this.escapeIdentifier(cls.name);
483
+ const classSelfType = `${cname}${classGenericContext.typeArguments}`;
484
+ const tsValueType = (p) => this.typeToTsFromPython(p.type, classGenericContext, 'value');
485
+ const wrapAlias = (body) => {
486
+ const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
487
+ return this.wrap(ts, ts, [cls.name]);
488
+ };
140
489
  if (cls.decorators.includes('__typed_dict__') || cls.kind === 'typed_dict') {
141
490
  const props = cls.properties
142
491
  .map(p => {
143
492
  const pname = this.escapeIdentifier(p.name);
144
493
  const opt = p.optional === true ? '?' : '';
145
- const t = this.typeToTs(this.mapper.mapPythonType(p.type, 'value'));
494
+ const t = this.typeToTsFromPython(p.type, classGenericContext, 'value');
146
495
  return `${pname}${opt}: ${t};`;
147
496
  })
148
497
  .join(' ');
149
- const cname = this.escapeIdentifier(cls.name);
150
- const ts = `${jsdoc}export type ${cname} = { ${props} }\n`;
151
- return this.wrap(ts, [cls.name]);
498
+ return wrapAlias(`{ ${props} }`);
152
499
  }
153
500
  if (cls.kind === 'namedtuple') {
154
- // NamedTuple -> readonly tuple type alias `[T1, T2, ...]`
155
- const elements = cls.properties.map(p => this.typeToTs(this.mapper.mapPythonType(p.type, 'value')));
156
- const cname = this.escapeIdentifier(cls.name);
157
- const ts = `${jsdoc}export type ${cname} = readonly [${elements.join(', ')}]\n`;
158
- return this.wrap(ts, [cls.name]);
501
+ const elements = cls.properties.map(p => this.typeToTsFromPython(p.type, classGenericContext, 'value'));
502
+ return wrapAlias(`readonly [${elements.join(', ')}]`);
159
503
  }
160
504
  if (cls.kind === 'protocol') {
161
- // Protocol -> structural interface-like type alias for attributes and callables (subset)
162
505
  const props = cls.properties
163
- .map(p => `${this.escapeIdentifier(p.name)}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))};`)
506
+ .map(p => `${this.escapeIdentifier(p.name)}: ${this.typeToTsFromPython(p.type, classGenericContext, 'value')};`)
164
507
  .join(' ');
165
508
  const methods = cls.methods
509
+ .filter(m => m.name !== '__init__')
166
510
  .map(m => {
167
511
  const fparams = m.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
512
+ const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(m.typeParameters), [m.returnType, ...fparams.map(param => param.type)], moduleName);
513
+ const methodGenericContext = this.mergeGenericRenderContexts(classGenericContext, methodOwnGenericContext);
514
+ const methodTypeParamDecl = methodOwnGenericContext.declaration;
168
515
  const paramsDecl = fparams
169
- .map(p => `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`)
516
+ .map(p => `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTsFromPython(p.type, methodGenericContext, 'value')}`)
170
517
  .join(', ');
171
- const returnType = this.typeToTs(this.mapper.mapPythonType(m.returnType, 'return'));
172
- return `${this.escapeIdentifier(m.name)}: (${paramsDecl}) => ${returnType};`;
518
+ const returnType = this.typeToTsFromPython(m.returnType, methodGenericContext, 'return');
519
+ return `${this.escapeIdentifier(m.name)}: ${methodTypeParamDecl}(${paramsDecl}) => ${returnType};`;
173
520
  })
174
521
  .join(' ');
175
- const cname = this.escapeIdentifier(cls.name);
176
- const ts = `${jsdoc}export type ${cname} = { ${props} ${methods} }\n`;
177
- return this.wrap(ts, [cls.name]);
522
+ return wrapAlias(`{ ${props} ${methods} }`);
178
523
  }
179
524
  if (cls.kind === 'dataclass' || cls.kind === 'pydantic') {
180
- // Data containers -> object type alias
181
525
  const props = cls.properties
182
526
  .map(p => {
183
527
  const pname = this.escapeIdentifier(p.name);
184
528
  const opt = p.optional === true ? '?' : '';
185
- const t = this.typeToTs(this.mapper.mapPythonType(p.type, 'value'));
529
+ const t = this.typeToTsFromPython(p.type, classGenericContext, 'value');
186
530
  return `${pname}${opt}: ${t};`;
187
531
  })
188
532
  .join(' ');
189
- const cname = this.escapeIdentifier(cls.name);
190
- const ts = `${jsdoc}export type ${cname} = { ${props} }\n`;
191
- return this.wrap(ts, [cls.name]);
533
+ return wrapAlias(`{ ${props} }`);
192
534
  }
193
535
  const sortedMethods = [...cls.methods].sort((a, b) => a.name.localeCompare(b.name));
194
- const methodBodies = sortedMethods
195
- .filter(m => m.name !== '__init__')
196
- .map(m => {
197
- const fparams = m.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
198
- const paramsDecl = fparams
199
- .map(p => p.varArgs
200
- ? `...${this.escapeIdentifier(p.name)}: unknown[]`
201
- : p.kwArgs
202
- ? `kwargs?: Record<string, unknown>`
203
- : `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`)
204
- .join(', ');
205
- const positionalParams = fparams.filter(p => !p.kwArgs);
206
- const callPassthrough = positionalParams
207
- .map(p => p.varArgs ? `...${this.escapeIdentifier(p.name)}` : this.escapeIdentifier(p.name))
208
- .join(', ');
209
- const hasKwArgs = fparams.some(p => p.kwArgs);
210
- const returnType = this.typeToTs(this.mapper.mapPythonType(m.returnType, 'return'));
211
- return ` async ${this.escapeIdentifier(m.name)}(${paramsDecl}): Promise<${returnType}> { return getRuntimeBridge().callMethod(this.__handle, '${m.name}', [${callPassthrough}]${hasKwArgs ? ', kwargs' : ''}); }`;
212
- })
213
- .join('\n');
214
- // Constructor typing from __init__
536
+ const methodBodies = [];
537
+ const methodDeclarations = [];
538
+ sortedMethods
539
+ .filter(method => method.name !== '__init__')
540
+ .forEach(method => {
541
+ const fparams = method.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
542
+ const methodOwnGenericContext = this.buildGenericRenderContext(this.getTypeParameters(method.typeParameters), [method.returnType, ...fparams.map(param => param.type)], moduleName);
543
+ const methodGenericContext = this.mergeGenericRenderContexts(classGenericContext, methodOwnGenericContext);
544
+ const methodTypeParamDecl = methodOwnGenericContext.declaration;
545
+ const methodTsValueType = (p) => this.typeToTsFromPython(p.type, methodGenericContext, 'value');
546
+ const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
547
+ const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
548
+ const hasVarKwArgs = fparams.some(p => p.kwArgs);
549
+ const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
550
+ const varArgsParam = fparams.find(p => p.varArgs);
551
+ const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
552
+ const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
553
+ const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
554
+ const requiredPosCount = firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
555
+ const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
556
+ const renderPositionalParam = (p, forceRequired = false) => {
557
+ const pname = this.escapeIdentifier(p.name);
558
+ const opt = !forceRequired && p.optional ? '?' : '';
559
+ return `${pname}${opt}: ${methodTsValueType(p)}`;
560
+ };
561
+ const kwargsType = (() => {
562
+ if (!needsKwargsParam) {
563
+ return '';
564
+ }
565
+ if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
566
+ return 'Record<string, unknown>';
567
+ }
568
+ const props = keywordOnlyParams
569
+ .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${methodTsValueType(p)};`)
570
+ .join(' ');
571
+ const obj = `{ ${props} }`;
572
+ return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
573
+ })();
574
+ const paramsDeclParts = [];
575
+ positionalParams.forEach(p => {
576
+ paramsDeclParts.push(renderPositionalParam(p));
577
+ });
578
+ if (varArgsParam) {
579
+ const vname = this.escapeIdentifier(varArgsParam.name);
580
+ paramsDeclParts.push(needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`);
581
+ }
582
+ if (needsKwargsParam) {
583
+ paramsDeclParts.push(`kwargs?: ${kwargsType}`);
584
+ }
585
+ const paramsDecl = paramsDeclParts.join(', ');
586
+ const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
587
+ const returnType = this.typeToTsFromPython(method.returnType, methodGenericContext, 'return');
588
+ const mname = this.escapeIdentifier(method.name);
589
+ const overloads = [];
590
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
591
+ const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
592
+ const requiredPosCount = firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
593
+ for (let i = requiredPosCount; i <= positionalParams.length; i++) {
594
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
595
+ const rest = [];
596
+ if (varArgsParam) {
597
+ const vname = this.escapeIdentifier(varArgsParam.name);
598
+ rest.push(needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`);
599
+ }
600
+ rest.push(`kwargs: ${kwargsType}`);
601
+ overloads.push(` ${mname}${methodTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${returnType}>;`);
602
+ if (varArgsParam && needsVarArgsArray) {
603
+ overloads.push(` ${mname}${methodTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${returnType}>;`);
604
+ }
605
+ }
606
+ }
607
+ const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
608
+ const callPreludeLines = [];
609
+ if (needsKwargsParam) {
610
+ callPreludeLines.push(` let __kwargs = kwargs;`);
611
+ }
612
+ const positionalArgExprs = positionalParams.map(p => this.escapeIdentifier(p.name));
613
+ callPreludeLines.push(` const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
614
+ if (requiredPosCount < positionalParams.length) {
615
+ callPreludeLines.push(` while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`);
616
+ callPreludeLines.push(` __args.pop();`);
617
+ callPreludeLines.push(` }`);
618
+ }
619
+ if (needsKwargsParam && requiredPosCount < positionalParams.length) {
620
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr('__candidate', {
621
+ keywordOnlyNames,
622
+ requiredKwOnlyNames,
623
+ hasVarKwArgs,
624
+ });
625
+ callPreludeLines.push(` if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`);
626
+ callPreludeLines.push(` const __candidate = __args[__args.length - 1];`);
627
+ callPreludeLines.push(` if (${looksLikeKwargs}) {`);
628
+ callPreludeLines.push(` __kwargs = __candidate as any;`);
629
+ callPreludeLines.push(` __args.pop();`);
630
+ callPreludeLines.push(` }`);
631
+ callPreludeLines.push(` }`);
632
+ }
633
+ if (varArgsParam) {
634
+ const vname = this.escapeIdentifier(varArgsParam.name);
635
+ if (needsVarArgsArray) {
636
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr(vname, {
637
+ keywordOnlyNames,
638
+ requiredKwOnlyNames,
639
+ hasVarKwArgs,
640
+ });
641
+ callPreludeLines.push(` let __varargs: unknown[] = [];`);
642
+ callPreludeLines.push(` if (${vname} !== undefined) {`);
643
+ callPreludeLines.push(` if (globalThis.Array.isArray(${vname})) {`);
644
+ callPreludeLines.push(` __varargs = ${vname};`);
645
+ callPreludeLines.push(` } else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
646
+ callPreludeLines.push(` __kwargs = ${vname} as any;`);
647
+ callPreludeLines.push(` } else {`);
648
+ callPreludeLines.push(` throw new Error(\`${method.name} expected ${varArgsParam.name} to be an array\`);`);
649
+ callPreludeLines.push(` }`);
650
+ callPreludeLines.push(` }`);
651
+ callPreludeLines.push(` __args.push(...__varargs);`);
652
+ }
653
+ else {
654
+ callPreludeLines.push(` __args.push(...${vname});`);
655
+ }
656
+ }
657
+ const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
658
+ const guardLines = [];
659
+ if (needsKwargsParam && positionalOnlyNames.length > 0) {
660
+ guardLines.push(` const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`);
661
+ guardLines.push(` for (const key of __positionalOnly) {`);
662
+ guardLines.push(` if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
663
+ guardLines.push(` throw new Error(\`${method.name} does not accept positional-only argument "\${key}" as a keyword argument\`);`);
664
+ guardLines.push(` }`);
665
+ guardLines.push(` }`);
666
+ }
667
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
668
+ guardLines.push(` const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`);
669
+ guardLines.push(` const __missing: string[] = [];`);
670
+ guardLines.push(` for (const key of __requiredKwOnly) {`);
671
+ guardLines.push(` if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
672
+ guardLines.push(` __missing.push(key);`);
673
+ guardLines.push(` }`);
674
+ guardLines.push(` }`);
675
+ guardLines.push(` if (__missing.length > 0) {`);
676
+ guardLines.push(` throw new Error(\`Missing required keyword-only arguments for ${method.name}: \${__missing.join(', ')}\`);`);
677
+ guardLines.push(` }`);
678
+ }
679
+ const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
680
+ methodBodies.push(`${overloadDecl} async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
681
+ ${callPrelude}${guards} return getRuntimeBridge().callMethod(this.__handle, '${method.name}', __args${needsKwargsParam ? ', __kwargs' : ''});
682
+ }`);
683
+ methodDeclarations.push(`${overloadDecl}${overloads.length > 0 ? '' : ` ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}>;\n`}`);
684
+ });
215
685
  const init = cls.methods.find(m => m.name === '__init__');
216
- const ctorParams = init
217
- ? init.parameters
218
- .filter(p => p.name !== 'self' && p.name !== 'cls')
219
- .map(p => p.varArgs
220
- ? `...${this.escapeIdentifier(p.name)}: unknown[]`
221
- : p.kwArgs
222
- ? `kwargs?: Record<string, unknown>`
223
- : `${this.escapeIdentifier(p.name)}${p.optional ? '?' : ''}: ${this.typeToTs(this.mapper.mapPythonType(p.type, 'value'))}`)
224
- .join(', ')
225
- : `...args: unknown[]`;
226
- const ctorHasKwArgs = init
227
- ? init.parameters.some(p => p.name !== 'self' && p.name !== 'cls' && p.kwArgs)
228
- : false;
229
- const ctorArgsPassthrough = init
230
- ? init.parameters
231
- .filter(p => p.name !== 'self' && p.name !== 'cls' && !p.kwArgs)
232
- .map(p => p.varArgs ? `...${this.escapeIdentifier(p.name)}` : this.escapeIdentifier(p.name))
233
- .join(', ')
234
- : '...args';
235
- const cname = this.escapeIdentifier(cls.name);
686
+ const ctorSpec = (() => {
687
+ if (!init) {
688
+ return {
689
+ overloadDecl: '',
690
+ declaration: ` static create${classTypeParamDecl}(...args: unknown[]): Promise<${classSelfType}>;\n`,
691
+ paramsDecl: `...args: unknown[]`,
692
+ callPrelude: ` const __args: unknown[] = [...args];\n`,
693
+ hasKwargs: false,
694
+ guardLines: [],
695
+ };
696
+ }
697
+ const fparams = init.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
698
+ const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
699
+ const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
700
+ const hasVarKwArgs = fparams.some(p => p.kwArgs);
701
+ const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
702
+ const varArgsParam = fparams.find(p => p.varArgs);
703
+ const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
704
+ const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
705
+ const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
706
+ const requiredPosCount = firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
707
+ const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
708
+ const renderPositionalParam = (p, forceRequired = false) => {
709
+ const pname = this.escapeIdentifier(p.name);
710
+ const opt = !forceRequired && p.optional ? '?' : '';
711
+ return `${pname}${opt}: ${tsValueType(p)}`;
712
+ };
713
+ const kwargsType = (() => {
714
+ if (!needsKwargsParam) {
715
+ return '';
716
+ }
717
+ if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
718
+ return 'Record<string, unknown>';
719
+ }
720
+ const props = keywordOnlyParams
721
+ .map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsValueType(p)};`)
722
+ .join(' ');
723
+ const obj = `{ ${props} }`;
724
+ return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
725
+ })();
726
+ const paramsDeclParts = [];
727
+ positionalParams.forEach(p => {
728
+ paramsDeclParts.push(renderPositionalParam(p));
729
+ });
730
+ if (varArgsParam) {
731
+ const vname = this.escapeIdentifier(varArgsParam.name);
732
+ paramsDeclParts.push(needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`);
733
+ }
734
+ if (needsKwargsParam) {
735
+ paramsDeclParts.push(`kwargs?: ${kwargsType}`);
736
+ }
737
+ const paramsDecl = paramsDeclParts.join(', ');
738
+ const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
739
+ const overloads = [];
740
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
741
+ const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
742
+ const requiredPosCount = firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
743
+ for (let i = requiredPosCount; i <= positionalParams.length; i++) {
744
+ const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
745
+ const rest = [];
746
+ if (varArgsParam) {
747
+ const vname = this.escapeIdentifier(varArgsParam.name);
748
+ rest.push(needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`);
749
+ }
750
+ rest.push(`kwargs: ${kwargsType}`);
751
+ overloads.push(` static create${classTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${classSelfType}>;`);
752
+ if (varArgsParam && needsVarArgsArray) {
753
+ overloads.push(` static create${classTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${classSelfType}>;`);
754
+ }
755
+ }
756
+ }
757
+ const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
758
+ const declaration = overloads.length > 0
759
+ ? overloadDecl
760
+ : ` static create${classTypeParamDecl}(${paramsDecl}): Promise<${classSelfType}>;\n`;
761
+ const guardLines = [];
762
+ const callPreludeLines = [];
763
+ if (needsKwargsParam) {
764
+ callPreludeLines.push(` let __kwargs = kwargs;`);
765
+ }
766
+ const positionalArgExprs = positionalParams.map(p => this.escapeIdentifier(p.name));
767
+ callPreludeLines.push(` const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
768
+ if (requiredPosCount < positionalParams.length) {
769
+ callPreludeLines.push(` while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`);
770
+ callPreludeLines.push(` __args.pop();`);
771
+ callPreludeLines.push(` }`);
772
+ }
773
+ if (needsKwargsParam && requiredPosCount < positionalParams.length) {
774
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr('__candidate', {
775
+ keywordOnlyNames,
776
+ requiredKwOnlyNames,
777
+ hasVarKwArgs,
778
+ });
779
+ callPreludeLines.push(` if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`);
780
+ callPreludeLines.push(` const __candidate = __args[__args.length - 1];`);
781
+ callPreludeLines.push(` if (${looksLikeKwargs}) {`);
782
+ callPreludeLines.push(` __kwargs = __candidate as any;`);
783
+ callPreludeLines.push(` __args.pop();`);
784
+ callPreludeLines.push(` }`);
785
+ callPreludeLines.push(` }`);
786
+ }
787
+ if (varArgsParam) {
788
+ const vname = this.escapeIdentifier(varArgsParam.name);
789
+ if (needsVarArgsArray) {
790
+ const looksLikeKwargs = this.renderLooksLikeKwargsExpr(vname, {
791
+ keywordOnlyNames,
792
+ requiredKwOnlyNames,
793
+ hasVarKwArgs,
794
+ });
795
+ callPreludeLines.push(` let __varargs: unknown[] = [];`);
796
+ callPreludeLines.push(` if (${vname} !== undefined) {`);
797
+ callPreludeLines.push(` if (globalThis.Array.isArray(${vname})) {`);
798
+ callPreludeLines.push(` __varargs = ${vname};`);
799
+ callPreludeLines.push(` } else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
800
+ callPreludeLines.push(` __kwargs = ${vname} as any;`);
801
+ callPreludeLines.push(` } else {`);
802
+ callPreludeLines.push(` throw new Error(\`__init__ expected ${varArgsParam.name} to be an array\`);`);
803
+ callPreludeLines.push(` }`);
804
+ callPreludeLines.push(` }`);
805
+ callPreludeLines.push(` __args.push(...__varargs);`);
806
+ }
807
+ else {
808
+ callPreludeLines.push(` __args.push(...${vname});`);
809
+ }
810
+ }
811
+ const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
812
+ if (needsKwargsParam && positionalOnlyNames.length > 0) {
813
+ guardLines.push(` const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`);
814
+ guardLines.push(` for (const key of __positionalOnly) {`);
815
+ guardLines.push(` if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
816
+ guardLines.push(` throw new Error(\`__init__ does not accept positional-only argument "\${key}" as a keyword argument\`);`);
817
+ guardLines.push(` }`);
818
+ guardLines.push(` }`);
819
+ }
820
+ if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
821
+ guardLines.push(` const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`);
822
+ guardLines.push(` const __missing: string[] = [];`);
823
+ guardLines.push(` for (const key of __requiredKwOnly) {`);
824
+ guardLines.push(` if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
825
+ guardLines.push(` __missing.push(key);`);
826
+ guardLines.push(` }`);
827
+ guardLines.push(` }`);
828
+ guardLines.push(` if (__missing.length > 0) {`);
829
+ guardLines.push(` throw new Error(\`Missing required keyword-only arguments for __init__: \${__missing.join(', ')}\`);`);
830
+ guardLines.push(` }`);
831
+ }
832
+ return {
833
+ overloadDecl,
834
+ declaration,
835
+ paramsDecl,
836
+ callPrelude,
837
+ hasKwargs: needsKwargsParam,
838
+ guardLines,
839
+ };
840
+ })();
236
841
  const moduleId = moduleName ?? '__main__';
237
- const methodsSection = methodBodies ? `\n${methodBodies}\n` : '\n';
238
- const ts = `${jsdoc}export class ${cname} {
842
+ const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
843
+ const declarationMethodsSection = methodDeclarations.length > 0 ? `\n${methodDeclarations.join('')}\n` : '\n';
844
+ const ctorGuards = ctorSpec.guardLines.length > 0 ? `${ctorSpec.guardLines.join('\n')}\n` : '';
845
+ const newClassExpr = `new ${cname}${classGenericContext.typeArguments}(handle)`;
846
+ const ts = `${jsdoc}export class ${cname}${classTypeParamDecl} {
239
847
  private readonly __handle: string;
240
848
  private constructor(handle: string) { this.__handle = handle; }
241
- static async create(${ctorParams}): Promise<${cname}> {
242
- const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', [${ctorArgsPassthrough}]${ctorHasKwArgs ? ', kwargs' : ''});
243
- return new ${cname}(handle);
849
+ ${ctorSpec.overloadDecl} static async create${classTypeParamDecl}(${ctorSpec.paramsDecl}): Promise<${classSelfType}> {
850
+ ${ctorSpec.callPrelude}${ctorGuards} const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', __args${ctorSpec.hasKwargs ? ', __kwargs' : ''});
851
+ return ${newClassExpr};
244
852
  }
245
- static fromHandle(handle: string): ${cname} { return new ${cname}(handle); }${methodsSection} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
853
+ static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType} { return ${newClassExpr}; }${methodsSection} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
246
854
  }
247
855
  `;
248
- return this.wrap(ts, [cls.name]);
856
+ const declaration = `${jsdoc}export class ${cname}${classTypeParamDecl} {
857
+ private readonly __handle: string;
858
+ private constructor(handle: string);
859
+ ${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType};${declarationMethodsSection} disposeHandle(): Promise<void>;
860
+ }
861
+ `;
862
+ return this.wrap(ts, declaration, [cls.name]);
863
+ }
864
+ generateTypeAlias(alias, moduleName) {
865
+ const genericContext = this.buildGenericRenderContext(this.getTypeParameters(alias.typeParameters), [alias.type], moduleName);
866
+ const aliasName = this.escapeIdentifier(alias.name, { preserveCase: true });
867
+ const body = this.typeToTsFromPython(alias.type, genericContext, 'value');
868
+ const ts = `export type ${aliasName}${genericContext.declaration} = ${body}\n`;
869
+ return this.wrap(ts, ts, [alias.name]);
249
870
  }
250
871
  /**
251
872
  * Generate TypeScript wrapper for Python module with caching
@@ -266,23 +887,33 @@ export class CodeGenerator {
266
887
  return result;
267
888
  }
268
889
  generateModuleDefinition(module, annotatedJSDoc = false) {
269
- const functionCodes = [...module.functions]
890
+ const functionResults = [...module.functions]
891
+ .sort((a, b) => a.name.localeCompare(b.name))
892
+ .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc));
893
+ const classResults = [...module.classes]
270
894
  .sort((a, b) => a.name.localeCompare(b.name))
271
- .map(f => this.generateFunctionWrapper(f, module.name, annotatedJSDoc).typescript)
272
- .join('\n');
273
- const classCodes = [...module.classes]
895
+ .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc));
896
+ const typeAliasResults = [...(module.typeAliases ?? [])]
274
897
  .sort((a, b) => a.name.localeCompare(b.name))
275
- .map(c => this.generateClassWrapper(c, module.name, annotatedJSDoc).typescript)
276
- .join('\n');
898
+ .map(alias => this.generateTypeAlias(alias, module.name));
899
+ const functionCodes = functionResults.map(result => result.typescript).join('\n');
900
+ const classCodes = classResults.map(result => result.typescript).join('\n');
901
+ const typeAliasCodes = typeAliasResults.map(result => result.typescript).join('\n');
277
902
  const header = `// Generated by tywrap\n// Module: ${module.name}\n// DO NOT EDIT MANUALLY\n\n`;
903
+ const declarationHeader = `// Generated by tywrap\n// Type Declarations\n// DO NOT EDIT MANUALLY\n\n`;
278
904
  const hasRuntimeClasses = module.classes.some(c => {
279
905
  const kind = c.kind ?? 'class';
280
906
  return kind === 'class' && !c.decorators.includes('__typed_dict__');
281
907
  });
282
908
  const needsRuntime = module.functions.length > 0 || hasRuntimeClasses;
283
909
  const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '';
284
- const ts = `${header + bridgeDecl + functionCodes}\n${classCodes}`;
285
- return this.wrap(ts, [module.name]);
910
+ const ts = `${`${header}${bridgeDecl}${functionCodes}\n${classCodes}\n${typeAliasCodes}`.trimEnd()}\n`;
911
+ const declaration = `${`${declarationHeader}${functionResults
912
+ .map(result => result.declaration)
913
+ .join('\n')}\n${classResults
914
+ .map(result => result.declaration)
915
+ .join('\n')}\n${typeAliasResults.map(result => result.declaration).join('\n')}`.trimEnd()}\n`;
916
+ return this.wrap(ts, declaration, [module.name]);
286
917
  }
287
918
  generateJsDoc(doc, paramAnnotations) {
288
919
  const lines = [];
@@ -300,10 +931,10 @@ export class CodeGenerator {
300
931
  }
301
932
  return `/**\n${lines.join('\n')}\n */\n`;
302
933
  }
303
- wrap(typescript, _sources) {
934
+ wrap(typescript, declaration, _sources) {
304
935
  return {
305
936
  typescript,
306
- declaration: '',
937
+ declaration,
307
938
  sourceMap: undefined,
308
939
  metadata: {
309
940
  generatedAt: new Date(),
@@ -358,6 +989,9 @@ export class CodeGenerator {
358
989
  }
359
990
  case 'custom': {
360
991
  const c = type;
992
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(c.name)) {
993
+ return 'unknown';
994
+ }
361
995
  return c.name;
362
996
  }
363
997
  case 'literal': {