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
@@ -0,0 +1,500 @@
1
+ import type { PythonGenericParameter, PythonType } from '../types/index.js';
2
+
3
+ export interface AnnotationParserOptions {
4
+ onUnknownTypeName?: (name: string) => void;
5
+ knownTypeVarNames?: Iterable<string>;
6
+ typeParameters?: readonly PythonGenericParameter[];
7
+ }
8
+
9
+ export function parseAnnotationToPythonType(
10
+ annotation: unknown,
11
+ options: AnnotationParserOptions = {}
12
+ ): PythonType {
13
+ const onUnknownTypeName = options.onUnknownTypeName;
14
+ const knownTypeVarNames = new Set(options.knownTypeVarNames ?? []);
15
+ const knownTypeParameters = new Map(
16
+ (options.typeParameters ?? []).map(param => [param.name, param] as const)
17
+ );
18
+ const modulePrefixes = ['', 'typing.', 'typing_extensions.', 'collections.abc.'] as const;
19
+
20
+ const unknownType = (): PythonType => ({ kind: 'custom', name: 'Any', module: 'typing' });
21
+
22
+ const recordUnknown = (name: string): void => {
23
+ try {
24
+ onUnknownTypeName?.(name);
25
+ } catch {
26
+ // ignore diagnostics hooks
27
+ }
28
+ };
29
+
30
+ const mapLiteral = (text: string): PythonType => {
31
+ const t = text.trim();
32
+ if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
33
+ return { kind: 'literal', value: t.slice(1, -1) } as PythonType;
34
+ }
35
+ if (t === 'True' || t === 'False') {
36
+ return { kind: 'literal', value: t === 'True' } as PythonType;
37
+ }
38
+ if (t === 'None') {
39
+ return { kind: 'literal', value: null } as PythonType;
40
+ }
41
+ const num = Number(t);
42
+ if (t !== '' && !Number.isNaN(num)) {
43
+ return { kind: 'literal', value: num } as PythonType;
44
+ }
45
+ return { kind: 'custom', name: t } as PythonType;
46
+ };
47
+
48
+ const mapKnownTypeParameter = (name: string): PythonType | null => {
49
+ const normalized = name.replace(/^~/, '').trim();
50
+ const param = knownTypeParameters.get(normalized);
51
+ if (!param) {
52
+ return null;
53
+ }
54
+ switch (param.kind) {
55
+ case 'typevar':
56
+ return {
57
+ kind: 'typevar',
58
+ name: param.name,
59
+ bound: param.bound,
60
+ constraints: param.constraints,
61
+ variance: param.variance,
62
+ } satisfies PythonType;
63
+ case 'paramspec':
64
+ return { kind: 'paramspec', name: param.name } satisfies PythonType;
65
+ case 'typevartuple':
66
+ return { kind: 'typevartuple', name: param.name } satisfies PythonType;
67
+ }
68
+ };
69
+
70
+ const mapSimpleName = (name: string): PythonType => {
71
+ const n = name.replace(/^(typing\.|typing_extensions\.|collections\.abc\.)/, '').trim();
72
+ const known = mapKnownTypeParameter(n);
73
+ if (known) {
74
+ return known;
75
+ }
76
+ if (knownTypeVarNames.has(n)) {
77
+ return { kind: 'typevar', name: n };
78
+ }
79
+
80
+ if (n === 'int' || n === 'float' || n === 'str' || n === 'bool' || n === 'bytes') {
81
+ return { kind: 'primitive', name: n };
82
+ }
83
+
84
+ if (
85
+ n === 'Any' ||
86
+ n === 'Never' ||
87
+ n === 'LiteralString' ||
88
+ n === 'ClassVar' ||
89
+ n === 'Final' ||
90
+ n === 'TypeAlias' ||
91
+ n === 'Required' ||
92
+ n === 'NotRequired'
93
+ ) {
94
+ recordUnknown(n);
95
+ }
96
+
97
+ if (n === 'None' || n.toLowerCase() === 'nonetype') {
98
+ return { kind: 'primitive', name: 'None' };
99
+ }
100
+ if (n === 'list' || n === 'List') {
101
+ return { kind: 'collection', name: 'list', itemTypes: [] };
102
+ }
103
+ if (n === 'dict' || n === 'Dict') {
104
+ return { kind: 'collection', name: 'dict', itemTypes: [] };
105
+ }
106
+ if (n === 'tuple' || n === 'Tuple') {
107
+ return { kind: 'collection', name: 'tuple', itemTypes: [] };
108
+ }
109
+ if (n === 'set' || n === 'Set') {
110
+ return { kind: 'collection', name: 'set', itemTypes: [] };
111
+ }
112
+ if (n === 'frozenset' || n === 'FrozenSet') {
113
+ return { kind: 'collection', name: 'frozenset', itemTypes: [] };
114
+ }
115
+ if (n.startsWith('~')) {
116
+ return { kind: 'typevar', name: n.slice(1) };
117
+ }
118
+ return { kind: 'custom', name: n };
119
+ };
120
+
121
+ const parseTypingFactoryName = (text: string, name: string): string | null => {
122
+ for (const prefix of modulePrefixes) {
123
+ const start = `${prefix}${name}(`;
124
+ if (!text.startsWith(start) || !text.endsWith(')')) {
125
+ continue;
126
+ }
127
+
128
+ const inner = text.slice(start.length, -1).trim();
129
+ if (inner.length < 2) {
130
+ return null;
131
+ }
132
+
133
+ const quote = inner[0];
134
+ if ((quote !== "'" && quote !== '"') || inner[inner.length - 1] !== quote) {
135
+ return null;
136
+ }
137
+
138
+ const commaIndex = inner.indexOf(',');
139
+ const quoted = commaIndex === -1 ? inner : inner.slice(0, commaIndex).trimEnd();
140
+ if (quoted.length < 2 || quoted[quoted.length - 1] !== quote) {
141
+ return null;
142
+ }
143
+
144
+ return quoted.slice(1, -1);
145
+ }
146
+
147
+ return null;
148
+ };
149
+
150
+ const splitQualifiedName = (raw: string): { name: string; module?: string } => {
151
+ const trimmed = raw.trim();
152
+ const parts = trimmed.split('.').filter(Boolean);
153
+ if (parts.length <= 1) {
154
+ return { name: trimmed };
155
+ }
156
+ const name = parts[parts.length - 1] ?? trimmed;
157
+ return { name, module: parts.slice(0, -1).join('.') || undefined };
158
+ };
159
+
160
+ const normalizeCollectionName = (
161
+ raw: string
162
+ ): { name: 'list' | 'dict' | 'tuple' | 'set' | 'frozenset'; inner?: string } | null => {
163
+ const m = raw.match(
164
+ /^(typing\.|typing_extensions\.)?(List|Dict|Tuple|Set|FrozenSet|list|dict|tuple|set|frozenset)\[(.*)\]$/
165
+ );
166
+ if (!m) {
167
+ return null;
168
+ }
169
+ const nameRaw = String(m[2] ?? '');
170
+ const nameLower = nameRaw.toLowerCase();
171
+ const name =
172
+ nameLower === 'list'
173
+ ? 'list'
174
+ : nameLower === 'dict'
175
+ ? 'dict'
176
+ : nameLower === 'tuple'
177
+ ? 'tuple'
178
+ : nameLower === 'frozenset'
179
+ ? 'frozenset'
180
+ : 'set';
181
+ return { name, inner: String(m[3] ?? '') };
182
+ };
183
+
184
+ const splitTopLevel = (input: string, sep: '|' | ','): string[] => {
185
+ const results: string[] = [];
186
+ let level = 0;
187
+ let cur = '';
188
+ let inQuote: "'" | '"' | null = null;
189
+ let escaped = false;
190
+ let guard = 0;
191
+
192
+ for (let i = 0; i < input.length; i++) {
193
+ guard++;
194
+ if (guard > 20000) {
195
+ if (cur.trim()) {
196
+ results.push(cur.trim());
197
+ }
198
+ break;
199
+ }
200
+
201
+ const ch = input.charAt(i);
202
+
203
+ if (escaped) {
204
+ cur += ch;
205
+ escaped = false;
206
+ continue;
207
+ }
208
+
209
+ if (inQuote) {
210
+ cur += ch;
211
+ if (ch === '\\') {
212
+ escaped = true;
213
+ continue;
214
+ }
215
+ if (ch === inQuote) {
216
+ inQuote = null;
217
+ }
218
+ continue;
219
+ }
220
+
221
+ if (ch === "'" || ch === '"') {
222
+ inQuote = ch;
223
+ cur += ch;
224
+ continue;
225
+ }
226
+
227
+ if (ch === '[' || ch === '(') {
228
+ level++;
229
+ } else if (ch === ']' || ch === ')') {
230
+ level = Math.max(0, level - 1);
231
+ }
232
+
233
+ if (level === 0 && ch === sep) {
234
+ results.push(cur.trim());
235
+ cur = '';
236
+ continue;
237
+ }
238
+
239
+ cur += ch;
240
+ }
241
+
242
+ if (cur.trim()) {
243
+ results.push(cur.trim());
244
+ }
245
+ return results;
246
+ };
247
+
248
+ const splitGenericInvocation = (raw: string): { name: string; inner: string } | null => {
249
+ if (!raw.endsWith(']')) {
250
+ return null;
251
+ }
252
+ const bracketStart = raw.indexOf('[');
253
+ if (bracketStart <= 0) {
254
+ return null;
255
+ }
256
+
257
+ let depth = 0;
258
+ for (let i = bracketStart; i < raw.length; i++) {
259
+ const ch = raw.charAt(i);
260
+ if (ch === '[') {
261
+ depth++;
262
+ } else if (ch === ']') {
263
+ depth--;
264
+ if (depth === 0 && i !== raw.length - 1) {
265
+ return null;
266
+ }
267
+ }
268
+ }
269
+
270
+ if (depth !== 0) {
271
+ return null;
272
+ }
273
+
274
+ return {
275
+ name: raw.slice(0, bracketStart).trim(),
276
+ inner: raw.slice(bracketStart + 1, -1),
277
+ };
278
+ };
279
+
280
+ const parse = (ann: unknown, depth = 0): PythonType => {
281
+ if (ann === null || ann === undefined) {
282
+ return unknownType();
283
+ }
284
+ if (depth > 100) {
285
+ return unknownType();
286
+ }
287
+ const rawText = String(ann).trim();
288
+ const raw = rawText.startsWith('~') ? rawText.slice(1).trim() : rawText;
289
+ if (raw === '') {
290
+ return unknownType();
291
+ }
292
+
293
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw) && rawText.startsWith('~')) {
294
+ return mapKnownTypeParameter(raw) ?? { kind: 'typevar', name: raw };
295
+ }
296
+
297
+ const paramspecArgsMatch = rawText.match(/^~?([A-Za-z_][A-Za-z0-9_]*)\.(args|kwargs)$/);
298
+ if (paramspecArgsMatch?.[1] && paramspecArgsMatch[2]) {
299
+ const baseName = paramspecArgsMatch[1];
300
+ const known = mapKnownTypeParameter(baseName);
301
+ if (known?.kind === 'paramspec' || rawText.startsWith('~')) {
302
+ return paramspecArgsMatch[2] === 'args'
303
+ ? ({ kind: 'paramspec_args', name: baseName } satisfies PythonType)
304
+ : ({ kind: 'paramspec_kwargs', name: baseName } satisfies PythonType);
305
+ }
306
+ }
307
+
308
+ const builtInClassMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
309
+ if (builtInClassMatch) {
310
+ const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
311
+ const name = (inner.split('.').pop() ?? '').toString();
312
+ return mapSimpleName(name);
313
+ }
314
+
315
+ if (raw.includes('|')) {
316
+ const parts = splitTopLevel(raw, '|');
317
+ if (parts.length > 1) {
318
+ const types = parts.map(p => parse(p.trim(), depth + 1));
319
+ return { kind: 'union', types };
320
+ }
321
+ }
322
+
323
+ if (
324
+ raw.startsWith('typing.Union[') ||
325
+ raw.startsWith('typing_extensions.Union[') ||
326
+ raw.startsWith('Union[')
327
+ ) {
328
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
329
+ const parts = splitTopLevel(inner, ',');
330
+ const types = parts.map(p => parse(p.trim(), depth + 1));
331
+ return { kind: 'union', types };
332
+ }
333
+
334
+ if (
335
+ raw.startsWith('typing.Optional[') ||
336
+ raw.startsWith('typing_extensions.Optional[') ||
337
+ raw.startsWith('Optional[')
338
+ ) {
339
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
340
+ return { kind: 'optional', type: parse(inner, depth + 1) };
341
+ }
342
+
343
+ if (
344
+ raw.startsWith('typing.Literal[') ||
345
+ raw.startsWith('typing_extensions.Literal[') ||
346
+ raw.startsWith('Literal[')
347
+ ) {
348
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
349
+ const parts = splitTopLevel(inner, ',');
350
+ if (parts.length === 1) {
351
+ return mapLiteral(String(parts[0] ?? '').trim());
352
+ }
353
+ return { kind: 'union', types: parts.map(p => mapLiteral(String(p).trim())) };
354
+ }
355
+
356
+ const extMatch = raw.match(
357
+ /^(typing\.|typing_extensions\.)?(ClassVar|Final|Required|NotRequired)\[(.*)\]$/
358
+ );
359
+ if (extMatch) {
360
+ return parse(extMatch[3] ?? '', depth + 1);
361
+ }
362
+
363
+ const typeVarName = parseTypingFactoryName(rawText, 'TypeVar');
364
+ if (typeVarName) {
365
+ return mapKnownTypeParameter(typeVarName) ?? { kind: 'typevar', name: typeVarName };
366
+ }
367
+
368
+ const paramSpecName = parseTypingFactoryName(rawText, 'ParamSpec');
369
+ if (paramSpecName) {
370
+ return mapKnownTypeParameter(paramSpecName) ?? { kind: 'paramspec', name: paramSpecName };
371
+ }
372
+
373
+ const typeVarTupleName = parseTypingFactoryName(rawText, 'TypeVarTuple');
374
+ if (typeVarTupleName) {
375
+ return (
376
+ mapKnownTypeParameter(typeVarTupleName) ?? {
377
+ kind: 'typevartuple',
378
+ name: typeVarTupleName,
379
+ }
380
+ );
381
+ }
382
+ if (
383
+ raw === 'typing.LiteralString' ||
384
+ raw === 'typing_extensions.LiteralString' ||
385
+ raw === 'LiteralString'
386
+ ) {
387
+ return { kind: 'primitive', name: 'str' };
388
+ }
389
+
390
+ if (
391
+ raw.startsWith('typing.Callable[') ||
392
+ raw.startsWith('typing_extensions.Callable[') ||
393
+ raw.startsWith('Callable[')
394
+ ) {
395
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
396
+ const parts = splitTopLevel(inner, ',');
397
+ if (parts.length >= 2) {
398
+ const paramsPart = (parts[0] ?? '').trim();
399
+ const returnPart = parts.slice(1).join(',').trim();
400
+ const returnType = parse(returnPart, depth + 1);
401
+
402
+ if (paramsPart === '...' || paramsPart === 'Ellipsis') {
403
+ return {
404
+ kind: 'callable',
405
+ parameters: [{ kind: 'custom', name: '...' }],
406
+ returnType,
407
+ };
408
+ }
409
+
410
+ const directParamSpec = parse(paramsPart, depth + 1);
411
+ if (directParamSpec.kind === 'paramspec') {
412
+ return {
413
+ kind: 'callable',
414
+ parameters: [],
415
+ parameterSpec: directParamSpec,
416
+ returnType,
417
+ };
418
+ }
419
+
420
+ const paramInner =
421
+ paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
422
+ const trimmed = paramInner.trim();
423
+ if (trimmed === '...' || trimmed === 'Ellipsis') {
424
+ return {
425
+ kind: 'callable',
426
+ parameters: [{ kind: 'custom', name: '...' }],
427
+ returnType,
428
+ };
429
+ }
430
+
431
+ const parameters = trimmed
432
+ ? splitTopLevel(trimmed, ',').map(p => parse(p.trim(), depth + 1))
433
+ : [];
434
+ return { kind: 'callable', parameters, returnType };
435
+ }
436
+ }
437
+
438
+ if (
439
+ raw.startsWith('typing.Mapping[') ||
440
+ raw.startsWith('typing_extensions.Mapping[') ||
441
+ raw.startsWith('Mapping[')
442
+ ) {
443
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
444
+ const parts = splitTopLevel(inner, ',');
445
+ return {
446
+ kind: 'collection',
447
+ name: 'dict',
448
+ itemTypes: parts.map(p => parse(p.trim(), depth + 1)),
449
+ };
450
+ }
451
+
452
+ if (
453
+ raw.startsWith('typing.Annotated[') ||
454
+ raw.startsWith('typing_extensions.Annotated[') ||
455
+ raw.startsWith('Annotated[')
456
+ ) {
457
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
458
+ const parts = splitTopLevel(inner, ',');
459
+ if (parts.length > 0) {
460
+ return {
461
+ kind: 'annotated',
462
+ base: parse(parts[0] ?? '', depth + 1),
463
+ metadata: parts.slice(1).map(p => String(p).trim()),
464
+ };
465
+ }
466
+ }
467
+
468
+ if (
469
+ raw.startsWith('typing.Unpack[') ||
470
+ raw.startsWith('typing_extensions.Unpack[') ||
471
+ raw.startsWith('Unpack[')
472
+ ) {
473
+ const inner = raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
474
+ return { kind: 'unpack', type: parse(inner, depth + 1) };
475
+ }
476
+
477
+ const coll = normalizeCollectionName(raw);
478
+ if (coll) {
479
+ const itemParts = splitTopLevel(coll.inner ?? '', ',');
480
+ const itemTypes = (coll.inner ? itemParts : []).map(p => parse(p.trim(), depth + 1));
481
+ return { kind: 'collection', name: coll.name, itemTypes };
482
+ }
483
+
484
+ const generic = splitGenericInvocation(raw);
485
+ if (generic) {
486
+ const typeArgs = splitTopLevel(generic.inner, ',').map(part => parse(part.trim(), depth + 1));
487
+ const qualified = splitQualifiedName(generic.name);
488
+ return {
489
+ kind: 'generic',
490
+ name: qualified.name,
491
+ module: qualified.module,
492
+ typeArgs,
493
+ };
494
+ }
495
+
496
+ return mapSimpleName(raw);
497
+ };
498
+
499
+ return parse(annotation, 0);
500
+ }
@@ -26,6 +26,7 @@ export interface DiscoveryOptions {
26
26
  searchPaths?: string[];
27
27
  excludePatterns?: string[];
28
28
  includeStdLib?: boolean;
29
+ timeoutMs?: number;
29
30
  }
30
31
 
31
32
  export class ModuleDiscovery {
@@ -39,6 +40,13 @@ export class ModuleDiscovery {
39
40
  includeStdLib: false,
40
41
  excludePatterns: ['__pycache__', '*.pyc', '.git', '.svn'],
41
42
  ...options,
43
+ // Avoid "no timeout by default" for subprocess discovery paths.
44
+ timeoutMs:
45
+ typeof options.timeoutMs === 'number' &&
46
+ Number.isFinite(options.timeoutMs) &&
47
+ options.timeoutMs > 0
48
+ ? options.timeoutMs
49
+ : 30000,
42
50
  };
43
51
  }
44
52
 
@@ -89,10 +97,14 @@ export class ModuleDiscovery {
89
97
  if (processUtils.isAvailable()) {
90
98
  try {
91
99
  const pythonPath = await this.getPythonExecutable();
92
- const result = await processUtils.exec(pythonPath, [
93
- '-c',
94
- `import ${moduleName}; print(${moduleName}.__file__ if hasattr(${moduleName}, '__file__') else '${moduleName}.__path__[0]' if hasattr(${moduleName}, '__path__') else 'builtin')`,
95
- ]);
100
+ const result = await processUtils.exec(
101
+ pythonPath,
102
+ [
103
+ '-c',
104
+ `import ${moduleName}; print(${moduleName}.__file__ if hasattr(${moduleName}, '__file__') else '${moduleName}.__path__[0]' if hasattr(${moduleName}, '__path__') else 'builtin')`,
105
+ ],
106
+ { timeoutMs: this.options.timeoutMs }
107
+ );
96
108
 
97
109
  if (result.code === 0 && result.stdout.trim() !== 'builtin') {
98
110
  const modulePath = result.stdout.trim();
@@ -297,10 +309,11 @@ export class ModuleDiscovery {
297
309
  if (processUtils.isAvailable()) {
298
310
  try {
299
311
  const pythonPath = await this.getPythonExecutable();
300
- const result = await processUtils.exec(pythonPath, [
301
- '-c',
302
- 'import sys; print("\\n".join(sys.path))',
303
- ]);
312
+ const result = await processUtils.exec(
313
+ pythonPath,
314
+ ['-c', 'import sys; print("\\n".join(sys.path))'],
315
+ { timeoutMs: this.options.timeoutMs }
316
+ );
304
317
 
305
318
  if (result.code === 0) {
306
319
  const sysPaths = result.stdout
@@ -376,10 +389,11 @@ export class ModuleDiscovery {
376
389
 
377
390
  try {
378
391
  const pythonPath = await this.getPythonExecutable();
379
- const result = await processUtils.exec(pythonPath, [
380
- '-c',
381
- `import ${moduleName}; print(getattr(${moduleName}, '__version__', 'unknown'))`,
382
- ]);
392
+ const result = await processUtils.exec(
393
+ pythonPath,
394
+ ['-c', `import ${moduleName}; print(getattr(${moduleName}, '__version__', 'unknown'))`],
395
+ { timeoutMs: this.options.timeoutMs }
396
+ );
383
397
 
384
398
  if (result.code === 0 && result.stdout.trim() !== 'unknown') {
385
399
  return result.stdout.trim();