tywrap 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +8 -6
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +236 -147
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +112 -97
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts +17 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +85 -49
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/emit-call.d.ts +58 -0
- package/dist/core/emit-call.d.ts.map +1 -0
- package/dist/core/emit-call.js +103 -0
- package/dist/core/emit-call.js.map +1 -0
- package/dist/core/generator.d.ts +2 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +54 -211
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +118 -104
- package/dist/core/mapper.js.map +1 -1
- package/dist/core/validation.d.ts +23 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +52 -48
- package/dist/core/validation.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +175 -133
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +6 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -31
- package/dist/index.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +2 -2
- package/dist/runtime/bounded-context.js +2 -2
- package/dist/runtime/{safe-codec.d.ts → bridge-codec.d.ts} +26 -11
- package/dist/runtime/bridge-codec.d.ts.map +1 -0
- package/dist/runtime/{safe-codec.js → bridge-codec.js} +69 -72
- package/dist/runtime/bridge-codec.js.map +1 -0
- package/dist/runtime/{http-io.d.ts → http-transport.d.ts} +6 -6
- package/dist/runtime/http-transport.d.ts.map +1 -0
- package/dist/runtime/{http-io.js → http-transport.js} +3 -3
- package/dist/runtime/http-transport.js.map +1 -0
- package/dist/runtime/http.d.ts +3 -3
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +4 -4
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/index.d.ts +9 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +6 -6
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -7
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +11 -11
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +10 -10
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/{pyodide-io.d.ts → pyodide-transport.d.ts} +8 -8
- package/dist/runtime/pyodide-transport.d.ts.map +1 -0
- package/dist/runtime/{pyodide-io.js → pyodide-transport.js} +5 -5
- package/dist/runtime/pyodide-transport.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +3 -3
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +5 -5
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +23 -5
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +36 -31
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +25 -9
- package/dist/runtime/subprocess-transport.d.ts.map +1 -0
- package/dist/runtime/{process-io.js → subprocess-transport.js} +60 -41
- package/dist/runtime/subprocess-transport.js.map +1 -0
- package/dist/runtime/{worker-pool.d.ts → transport-pool.d.ts} +15 -15
- package/dist/runtime/transport-pool.d.ts.map +1 -0
- package/dist/runtime/{worker-pool.js → transport-pool.js} +6 -6
- package/dist/runtime/transport-pool.js.map +1 -0
- package/dist/runtime/transport.d.ts +14 -8
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +10 -4
- package/dist/runtime/transport.js.map +1 -1
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +0 -5
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +0 -20
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +13 -2
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +52 -60
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +181 -182
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/python.d.ts.map +1 -1
- package/dist/utils/python.js +28 -17
- package/dist/utils/python.js.map +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +22 -16
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -2
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/python_bridge.py +54 -6
- package/runtime/safe_codec.py +8 -8
- package/runtime/tywrap_bridge_core.py +159 -23
- package/src/cli.ts +8 -12
- package/src/config/index.ts +271 -165
- package/src/core/annotation-parser.ts +135 -123
- package/src/core/discovery.ts +102 -54
- package/src/core/emit-call.ts +171 -0
- package/src/core/generator.ts +67 -263
- package/src/core/mapper.ts +125 -110
- package/src/core/validation.ts +84 -48
- package/src/dev.ts +237 -153
- package/src/index.ts +11 -68
- package/src/runtime/bounded-context.ts +2 -2
- package/src/runtime/{safe-codec.ts → bridge-codec.ts} +83 -95
- package/src/runtime/{http-io.ts → http-transport.ts} +5 -5
- package/src/runtime/http.ts +5 -5
- package/src/runtime/index.ts +23 -1
- package/src/runtime/node.ts +15 -15
- package/src/runtime/pooled-transport.ts +15 -15
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/{pyodide-io.ts → pyodide-transport.ts} +7 -7
- package/src/runtime/pyodide.ts +6 -6
- package/src/runtime/rpc-client.ts +51 -40
- package/src/runtime/{process-io.ts → subprocess-transport.ts} +77 -47
- package/src/runtime/{worker-pool.ts → transport-pool.ts} +26 -26
- package/src/runtime/transport.ts +15 -8
- package/src/types/index.ts +10 -1
- package/src/tywrap.ts +0 -24
- package/src/utils/cache.ts +61 -63
- package/src/utils/codec.ts +217 -196
- package/src/utils/python.ts +33 -20
- package/src/utils/runtime.ts +24 -13
- package/src/version.ts +12 -0
- package/dist/runtime/http-io.d.ts.map +0 -1
- package/dist/runtime/http-io.js.map +0 -1
- package/dist/runtime/optimized-node.d.ts +0 -23
- package/dist/runtime/optimized-node.d.ts.map +0 -1
- package/dist/runtime/optimized-node.js +0 -23
- package/dist/runtime/optimized-node.js.map +0 -1
- package/dist/runtime/process-io.d.ts.map +0 -1
- package/dist/runtime/process-io.js.map +0 -1
- package/dist/runtime/protocol.d.ts +0 -3
- package/dist/runtime/protocol.d.ts.map +0 -1
- package/dist/runtime/protocol.js +0 -3
- package/dist/runtime/protocol.js.map +0 -1
- package/dist/runtime/pyodide-io.d.ts.map +0 -1
- package/dist/runtime/pyodide-io.js.map +0 -1
- package/dist/runtime/safe-codec.d.ts.map +0 -1
- package/dist/runtime/safe-codec.js.map +0 -1
- package/dist/runtime/worker-pool.d.ts.map +0 -1
- package/dist/runtime/worker-pool.js.map +0 -1
- package/dist/utils/bundle-optimizer.d.ts +0 -182
- package/dist/utils/bundle-optimizer.d.ts.map +0 -1
- package/dist/utils/bundle-optimizer.js +0 -680
- package/dist/utils/bundle-optimizer.js.map +0 -1
- package/dist/utils/memory-profiler.d.ts +0 -123
- package/dist/utils/memory-profiler.d.ts.map +0 -1
- package/dist/utils/memory-profiler.js +0 -426
- package/dist/utils/memory-profiler.js.map +0 -1
- package/src/runtime/optimized-node.ts +0 -25
- package/src/runtime/protocol.ts +0 -2
- package/src/utils/bundle-optimizer.ts +0 -876
- package/src/utils/memory-profiler.ts +0 -582
|
@@ -118,30 +118,35 @@ export function parseAnnotationToPythonType(
|
|
|
118
118
|
return { kind: 'custom', name: n };
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
// Extracts the leading quoted string from the body of a `Name(...)` factory call
|
|
122
|
+
// (the `inner` already stripped of the outer `Name(` and trailing `)`). Returns
|
|
123
|
+
// the unquoted name, or null when the body is not a well-formed quoted argument.
|
|
124
|
+
const extractQuotedFactoryArg = (inner: string): string | null => {
|
|
125
|
+
if (inner.length < 2) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const quote = inner[0];
|
|
130
|
+
if ((quote !== "'" && quote !== '"') || inner[inner.length - 1] !== quote) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const commaIndex = inner.indexOf(',');
|
|
135
|
+
const quoted = commaIndex === -1 ? inner : inner.slice(0, commaIndex).trimEnd();
|
|
136
|
+
if (quoted.length < 2 || quoted[quoted.length - 1] !== quote) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return quoted.slice(1, -1);
|
|
141
|
+
};
|
|
142
|
+
|
|
121
143
|
const parseTypingFactoryName = (text: string, name: string): string | null => {
|
|
122
144
|
for (const prefix of modulePrefixes) {
|
|
123
145
|
const start = `${prefix}${name}(`;
|
|
124
146
|
if (!text.startsWith(start) || !text.endsWith(')')) {
|
|
125
147
|
continue;
|
|
126
148
|
}
|
|
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);
|
|
149
|
+
return extractQuotedFactoryArg(text.slice(start.length, -1).trim());
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
return null;
|
|
@@ -245,6 +250,19 @@ export function parseAnnotationToPythonType(
|
|
|
245
250
|
return results;
|
|
246
251
|
};
|
|
247
252
|
|
|
253
|
+
// Returns the content between the first `[` and the last `]` of a special-form
|
|
254
|
+
// annotation (e.g. the `int, str` of `Union[int, str]`). Callers gate this on a
|
|
255
|
+
// prefix check, so the brackets are known to be present.
|
|
256
|
+
const bracketInner = (raw: string): string => raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
257
|
+
|
|
258
|
+
// True when `raw` opens with one of the supported module prefixes followed by
|
|
259
|
+
// `name[`. Mirrors the inlined `raw.startsWith('typing.Name[') || ...` checks so
|
|
260
|
+
// the branch ordering in `parse` is unchanged.
|
|
261
|
+
const startsWithSpecialForm = (raw: string, name: string): boolean =>
|
|
262
|
+
raw.startsWith(`typing.${name}[`) ||
|
|
263
|
+
raw.startsWith(`typing_extensions.${name}[`) ||
|
|
264
|
+
raw.startsWith(`${name}[`);
|
|
265
|
+
|
|
248
266
|
const splitGenericInvocation = (raw: string): { name: string; inner: string } | null => {
|
|
249
267
|
if (!raw.endsWith(']')) {
|
|
250
268
|
return null;
|
|
@@ -277,6 +295,86 @@ export function parseAnnotationToPythonType(
|
|
|
277
295
|
};
|
|
278
296
|
};
|
|
279
297
|
|
|
298
|
+
// Parses the body of a `Callable[...]` form (the `inner` already stripped of the
|
|
299
|
+
// outer brackets). `recurse` is the top-level `parse`, threaded in so this stays a
|
|
300
|
+
// standalone helper. Returns null only when the form has fewer than the required
|
|
301
|
+
// two comma-separated sections, matching the original guarded branch exactly.
|
|
302
|
+
const parseCallableInner = (
|
|
303
|
+
inner: string,
|
|
304
|
+
depth: number,
|
|
305
|
+
recurse: (ann: unknown, depth?: number) => PythonType
|
|
306
|
+
): PythonType | null => {
|
|
307
|
+
const parts = splitTopLevel(inner, ',');
|
|
308
|
+
if (parts.length < 2) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const paramsPart = (parts[0] ?? '').trim();
|
|
313
|
+
const returnPart = parts.slice(1).join(',').trim();
|
|
314
|
+
const returnType = recurse(returnPart, depth + 1);
|
|
315
|
+
|
|
316
|
+
if (paramsPart === '...' || paramsPart === 'Ellipsis') {
|
|
317
|
+
return {
|
|
318
|
+
kind: 'callable',
|
|
319
|
+
parameters: [{ kind: 'custom', name: '...' }],
|
|
320
|
+
returnType,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const directParamSpec = recurse(paramsPart, depth + 1);
|
|
325
|
+
if (directParamSpec.kind === 'paramspec') {
|
|
326
|
+
return {
|
|
327
|
+
kind: 'callable',
|
|
328
|
+
parameters: [],
|
|
329
|
+
parameterSpec: directParamSpec,
|
|
330
|
+
returnType,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const paramInner =
|
|
335
|
+
paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
|
|
336
|
+
const trimmed = paramInner.trim();
|
|
337
|
+
if (trimmed === '...' || trimmed === 'Ellipsis') {
|
|
338
|
+
return {
|
|
339
|
+
kind: 'callable',
|
|
340
|
+
parameters: [{ kind: 'custom', name: '...' }],
|
|
341
|
+
returnType,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const parameters = trimmed
|
|
346
|
+
? splitTopLevel(trimmed, ',').map(p => recurse(p.trim(), depth + 1))
|
|
347
|
+
: [];
|
|
348
|
+
return { kind: 'callable', parameters, returnType };
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Handles the `typing.TypeVar('T')` / `ParamSpec('P')` / `TypeVarTuple('Ts')`
|
|
352
|
+
// factory-call forms. Returns null when `rawText` is none of them, preserving
|
|
353
|
+
// the original sequential fall-through (TypeVar, then ParamSpec, then TypeVarTuple).
|
|
354
|
+
const parseTypingFactory = (rawText: string): PythonType | null => {
|
|
355
|
+
const typeVarName = parseTypingFactoryName(rawText, 'TypeVar');
|
|
356
|
+
if (typeVarName) {
|
|
357
|
+
return mapKnownTypeParameter(typeVarName) ?? { kind: 'typevar', name: typeVarName };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const paramSpecName = parseTypingFactoryName(rawText, 'ParamSpec');
|
|
361
|
+
if (paramSpecName) {
|
|
362
|
+
return mapKnownTypeParameter(paramSpecName) ?? { kind: 'paramspec', name: paramSpecName };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const typeVarTupleName = parseTypingFactoryName(rawText, 'TypeVarTuple');
|
|
366
|
+
if (typeVarTupleName) {
|
|
367
|
+
return (
|
|
368
|
+
mapKnownTypeParameter(typeVarTupleName) ?? {
|
|
369
|
+
kind: 'typevartuple',
|
|
370
|
+
name: typeVarTupleName,
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return null;
|
|
376
|
+
};
|
|
377
|
+
|
|
280
378
|
const parse = (ann: unknown, depth = 0): PythonType => {
|
|
281
379
|
if (ann === null || ann === undefined) {
|
|
282
380
|
return unknownType();
|
|
@@ -320,33 +418,18 @@ export function parseAnnotationToPythonType(
|
|
|
320
418
|
}
|
|
321
419
|
}
|
|
322
420
|
|
|
323
|
-
if (
|
|
324
|
-
raw
|
|
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, ',');
|
|
421
|
+
if (startsWithSpecialForm(raw, 'Union')) {
|
|
422
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
330
423
|
const types = parts.map(p => parse(p.trim(), depth + 1));
|
|
331
424
|
return { kind: 'union', types };
|
|
332
425
|
}
|
|
333
426
|
|
|
334
|
-
if (
|
|
335
|
-
|
|
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) };
|
|
427
|
+
if (startsWithSpecialForm(raw, 'Optional')) {
|
|
428
|
+
return { kind: 'optional', type: parse(bracketInner(raw), depth + 1) };
|
|
341
429
|
}
|
|
342
430
|
|
|
343
|
-
if (
|
|
344
|
-
raw
|
|
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, ',');
|
|
431
|
+
if (startsWithSpecialForm(raw, 'Literal')) {
|
|
432
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
350
433
|
if (parts.length === 1) {
|
|
351
434
|
return mapLiteral(String(parts[0] ?? '').trim());
|
|
352
435
|
}
|
|
@@ -360,24 +443,9 @@ export function parseAnnotationToPythonType(
|
|
|
360
443
|
return parse(extMatch[3] ?? '', depth + 1);
|
|
361
444
|
}
|
|
362
445
|
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
return
|
|
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
|
-
);
|
|
446
|
+
const typingFactory = parseTypingFactory(rawText);
|
|
447
|
+
if (typingFactory) {
|
|
448
|
+
return typingFactory;
|
|
381
449
|
}
|
|
382
450
|
if (
|
|
383
451
|
raw === 'typing.LiteralString' ||
|
|
@@ -387,61 +455,15 @@ export function parseAnnotationToPythonType(
|
|
|
387
455
|
return { kind: 'primitive', name: 'str' };
|
|
388
456
|
}
|
|
389
457
|
|
|
390
|
-
if (
|
|
391
|
-
raw
|
|
392
|
-
|
|
393
|
-
|
|
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 };
|
|
458
|
+
if (startsWithSpecialForm(raw, 'Callable')) {
|
|
459
|
+
const callable = parseCallableInner(bracketInner(raw), depth, parse);
|
|
460
|
+
if (callable) {
|
|
461
|
+
return callable;
|
|
435
462
|
}
|
|
436
463
|
}
|
|
437
464
|
|
|
438
|
-
if (
|
|
439
|
-
raw
|
|
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, ',');
|
|
465
|
+
if (startsWithSpecialForm(raw, 'Mapping')) {
|
|
466
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
445
467
|
return {
|
|
446
468
|
kind: 'collection',
|
|
447
469
|
name: 'dict',
|
|
@@ -449,13 +471,8 @@ export function parseAnnotationToPythonType(
|
|
|
449
471
|
};
|
|
450
472
|
}
|
|
451
473
|
|
|
452
|
-
if (
|
|
453
|
-
raw
|
|
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, ',');
|
|
474
|
+
if (startsWithSpecialForm(raw, 'Annotated')) {
|
|
475
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
459
476
|
if (parts.length > 0) {
|
|
460
477
|
return {
|
|
461
478
|
kind: 'annotated',
|
|
@@ -465,13 +482,8 @@ export function parseAnnotationToPythonType(
|
|
|
465
482
|
}
|
|
466
483
|
}
|
|
467
484
|
|
|
468
|
-
if (
|
|
469
|
-
|
|
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) };
|
|
485
|
+
if (startsWithSpecialForm(raw, 'Unpack')) {
|
|
486
|
+
return { kind: 'unpack', type: parse(bracketInner(raw), depth + 1) };
|
|
475
487
|
}
|
|
476
488
|
|
|
477
489
|
const coll = normalizeCollectionName(raw);
|
package/src/core/discovery.ts
CHANGED
|
@@ -12,6 +12,32 @@ import { getComponentLogger } from '../utils/logger.js';
|
|
|
12
12
|
|
|
13
13
|
const log = getComponentLogger('Discovery');
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Strict allow-list for Python module names.
|
|
17
|
+
*
|
|
18
|
+
* Module names are interpolated into a `python -c` source string, so anything
|
|
19
|
+
* outside a conservative identifier-plus-dots shape (e.g. quotes, semicolons,
|
|
20
|
+
* newlines, parentheses, whitespace) could break out of the intended
|
|
21
|
+
* expression and inject arbitrary code into the interpreter. Reject those
|
|
22
|
+
* before they ever reach the subprocess argv.
|
|
23
|
+
*/
|
|
24
|
+
const SAFE_MODULE_NAME = /^[A-Za-z_][A-Za-z0-9_.]*$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate a Python module name against {@link SAFE_MODULE_NAME}.
|
|
28
|
+
*
|
|
29
|
+
* @throws {Error} if the name is empty or contains characters that could
|
|
30
|
+
* escape the `python -c` expression it is interpolated into.
|
|
31
|
+
*/
|
|
32
|
+
function assertSafeModuleName(moduleName: string): void {
|
|
33
|
+
if (typeof moduleName !== 'string' || !SAFE_MODULE_NAME.test(moduleName)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Unsafe Python module name rejected: ${JSON.stringify(moduleName)}. ` +
|
|
36
|
+
'Module names must match /^[A-Za-z_][A-Za-z0-9_.]*$/.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
export interface ModuleInfo {
|
|
16
42
|
name: string;
|
|
17
43
|
path: string;
|
|
@@ -87,44 +113,70 @@ export class ModuleDiscovery {
|
|
|
87
113
|
* Resolve Python module path from module name
|
|
88
114
|
*/
|
|
89
115
|
async resolvePythonPath(moduleName: string): Promise<string | null> {
|
|
116
|
+
// Reject names that could break out of the `python -c` expression below.
|
|
117
|
+
assertSafeModuleName(moduleName);
|
|
118
|
+
|
|
90
119
|
// Check cache first
|
|
91
120
|
const cached = this.moduleCache.get(moduleName);
|
|
92
121
|
if (cached) {
|
|
93
122
|
return cached.path;
|
|
94
123
|
}
|
|
95
124
|
|
|
96
|
-
// Try to resolve using Python's module finder
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
// Try to resolve using Python's module finder, then fall back to a
|
|
126
|
+
// filesystem search across the known Python paths.
|
|
127
|
+
return (
|
|
128
|
+
(await this.resolveViaInterpreter(moduleName)) ??
|
|
129
|
+
(await this.resolveViaSearchPaths(moduleName))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a module path by asking the Python interpreter for `__file__`.
|
|
135
|
+
* Caches and returns the resolved path, or `null` when the interpreter is
|
|
136
|
+
* unavailable, reports a builtin, or fails.
|
|
137
|
+
*/
|
|
138
|
+
private async resolveViaInterpreter(moduleName: string): Promise<string | null> {
|
|
139
|
+
if (!processUtils.isAvailable()) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const pythonPath = await this.getPythonExecutable();
|
|
145
|
+
const result = await processUtils.exec(
|
|
146
|
+
pythonPath,
|
|
147
|
+
[
|
|
148
|
+
'-c',
|
|
149
|
+
`import ${moduleName}; print(${moduleName}.__file__ if hasattr(${moduleName}, '__file__') else '${moduleName}.__path__[0]' if hasattr(${moduleName}, '__path__') else 'builtin')`,
|
|
150
|
+
],
|
|
151
|
+
{ timeoutMs: this.options.timeoutMs }
|
|
152
|
+
);
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
if (result.code === 0 && result.stdout.trim() !== 'builtin') {
|
|
155
|
+
const modulePath = result.stdout.trim();
|
|
111
156
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
// Cache the result
|
|
158
|
+
this.moduleCache.set(moduleName, {
|
|
159
|
+
name: moduleName,
|
|
160
|
+
path: modulePath,
|
|
161
|
+
isPackage: modulePath.endsWith('__init__.py'),
|
|
162
|
+
dependencies: [],
|
|
163
|
+
});
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
} catch (error) {
|
|
123
|
-
log.warn('Failed to resolve module', { moduleName, error: String(error) });
|
|
165
|
+
return modulePath;
|
|
124
166
|
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
log.warn('Failed to resolve module', { moduleName, error: String(error) });
|
|
125
169
|
}
|
|
126
170
|
|
|
127
|
-
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fallback resolution: probe `<path>/<module>.py` and
|
|
176
|
+
* `<path>/<module>/__init__.py` across the known Python search paths.
|
|
177
|
+
* Returns the first existing candidate, or `null`.
|
|
178
|
+
*/
|
|
179
|
+
private async resolveViaSearchPaths(moduleName: string): Promise<string | null> {
|
|
128
180
|
const searchPaths = await this.getPythonSearchPaths();
|
|
129
181
|
|
|
130
182
|
for (const searchPath of searchPaths) {
|
|
@@ -208,40 +260,33 @@ export class ModuleDiscovery {
|
|
|
208
260
|
*/
|
|
209
261
|
parseDependenciesFromSource(source: string): string[] {
|
|
210
262
|
const dependencies: string[] = [];
|
|
211
|
-
const lines = source.split('\n');
|
|
212
263
|
|
|
213
|
-
for (const line of
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (trimmed.startsWith('#') || trimmed === '') {
|
|
218
|
-
continue;
|
|
264
|
+
for (const line of source.split('\n')) {
|
|
265
|
+
const moduleName = this.extractImportedModule(line.trim());
|
|
266
|
+
if (moduleName) {
|
|
267
|
+
dependencies.push(moduleName);
|
|
219
268
|
}
|
|
269
|
+
}
|
|
220
270
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (importMatch?.[1]) {
|
|
224
|
-
const fullModuleName = importMatch[1];
|
|
225
|
-
const moduleName = fullModuleName.split('.')[0]; // Get top-level module
|
|
226
|
-
if (moduleName) {
|
|
227
|
-
dependencies.push(moduleName);
|
|
228
|
-
}
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
271
|
+
return [...new Set(dependencies)]; // Remove duplicates
|
|
272
|
+
}
|
|
231
273
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
274
|
+
/**
|
|
275
|
+
* Extract the top-level module name imported by a single (trimmed) source
|
|
276
|
+
* line, or `null` when the line is a comment, blank, or not an import.
|
|
277
|
+
*/
|
|
278
|
+
private extractImportedModule(trimmed: string): string | null {
|
|
279
|
+
// Skip comments and empty lines
|
|
280
|
+
if (trimmed.startsWith('#') || trimmed === '') {
|
|
281
|
+
return null;
|
|
242
282
|
}
|
|
243
283
|
|
|
244
|
-
|
|
284
|
+
// Match `import x.y.z` and `from x.y.z import ...` statements.
|
|
285
|
+
const match =
|
|
286
|
+
trimmed.match(/^import\s+([a-zA-Z_][a-zA-Z0-9_\.]*)/) ??
|
|
287
|
+
trimmed.match(/^from\s+([a-zA-Z_][a-zA-Z0-9_\.]*)\s+import/);
|
|
288
|
+
|
|
289
|
+
return match?.[1]?.split('.')[0] ?? null; // Get top-level module
|
|
245
290
|
}
|
|
246
291
|
|
|
247
292
|
/**
|
|
@@ -383,6 +428,9 @@ export class ModuleDiscovery {
|
|
|
383
428
|
* Get module version information
|
|
384
429
|
*/
|
|
385
430
|
async getModuleVersion(moduleName: string): Promise<string | undefined> {
|
|
431
|
+
// Reject names that could break out of the `python -c` expression below.
|
|
432
|
+
assertSafeModuleName(moduleName);
|
|
433
|
+
|
|
386
434
|
if (!processUtils.isAvailable()) {
|
|
387
435
|
return undefined;
|
|
388
436
|
}
|