tywrap 0.5.1 → 0.6.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.
- 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 +93 -83
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +26 -0
- 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/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} +11 -11
- package/dist/runtime/bridge-codec.d.ts.map +1 -0
- package/dist/runtime/{safe-codec.js → bridge-codec.js} +26 -26
- 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 +5 -5
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +6 -7
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +9 -9
- package/dist/runtime/subprocess-transport.d.ts.map +1 -0
- package/dist/runtime/{process-io.js → subprocess-transport.js} +6 -6
- 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/utils/cache.d.ts +2 -2
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +2 -2
- 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/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 +112 -105
- package/src/core/discovery.ts +32 -0
- package/src/core/emit-call.ts +171 -0
- package/src/core/generator.ts +67 -263
- package/src/core/mapper.ts +125 -110
- package/src/index.ts +11 -68
- package/src/runtime/bounded-context.ts +2 -2
- package/src/runtime/{safe-codec.ts → bridge-codec.ts} +27 -27
- 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 +12 -8
- package/src/runtime/{process-io.ts → subprocess-transport.ts} +8 -8
- 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/utils/cache.ts +2 -2
- package/src/utils/codec.ts +217 -196
- 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
|
@@ -245,6 +245,19 @@ export function parseAnnotationToPythonType(
|
|
|
245
245
|
return results;
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
+
// Returns the content between the first `[` and the last `]` of a special-form
|
|
249
|
+
// annotation (e.g. the `int, str` of `Union[int, str]`). Callers gate this on a
|
|
250
|
+
// prefix check, so the brackets are known to be present.
|
|
251
|
+
const bracketInner = (raw: string): string => raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
252
|
+
|
|
253
|
+
// True when `raw` opens with one of the supported module prefixes followed by
|
|
254
|
+
// `name[`. Mirrors the inlined `raw.startsWith('typing.Name[') || ...` checks so
|
|
255
|
+
// the branch ordering in `parse` is unchanged.
|
|
256
|
+
const startsWithSpecialForm = (raw: string, name: string): boolean =>
|
|
257
|
+
raw.startsWith(`typing.${name}[`) ||
|
|
258
|
+
raw.startsWith(`typing_extensions.${name}[`) ||
|
|
259
|
+
raw.startsWith(`${name}[`);
|
|
260
|
+
|
|
248
261
|
const splitGenericInvocation = (raw: string): { name: string; inner: string } | null => {
|
|
249
262
|
if (!raw.endsWith(']')) {
|
|
250
263
|
return null;
|
|
@@ -277,6 +290,86 @@ export function parseAnnotationToPythonType(
|
|
|
277
290
|
};
|
|
278
291
|
};
|
|
279
292
|
|
|
293
|
+
// Parses the body of a `Callable[...]` form (the `inner` already stripped of the
|
|
294
|
+
// outer brackets). `recurse` is the top-level `parse`, threaded in so this stays a
|
|
295
|
+
// standalone helper. Returns null only when the form has fewer than the required
|
|
296
|
+
// two comma-separated sections, matching the original guarded branch exactly.
|
|
297
|
+
const parseCallableInner = (
|
|
298
|
+
inner: string,
|
|
299
|
+
depth: number,
|
|
300
|
+
recurse: (ann: unknown, depth?: number) => PythonType
|
|
301
|
+
): PythonType | null => {
|
|
302
|
+
const parts = splitTopLevel(inner, ',');
|
|
303
|
+
if (parts.length < 2) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const paramsPart = (parts[0] ?? '').trim();
|
|
308
|
+
const returnPart = parts.slice(1).join(',').trim();
|
|
309
|
+
const returnType = recurse(returnPart, depth + 1);
|
|
310
|
+
|
|
311
|
+
if (paramsPart === '...' || paramsPart === 'Ellipsis') {
|
|
312
|
+
return {
|
|
313
|
+
kind: 'callable',
|
|
314
|
+
parameters: [{ kind: 'custom', name: '...' }],
|
|
315
|
+
returnType,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const directParamSpec = recurse(paramsPart, depth + 1);
|
|
320
|
+
if (directParamSpec.kind === 'paramspec') {
|
|
321
|
+
return {
|
|
322
|
+
kind: 'callable',
|
|
323
|
+
parameters: [],
|
|
324
|
+
parameterSpec: directParamSpec,
|
|
325
|
+
returnType,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const paramInner =
|
|
330
|
+
paramsPart.startsWith('[') && paramsPart.endsWith(']') ? paramsPart.slice(1, -1) : '';
|
|
331
|
+
const trimmed = paramInner.trim();
|
|
332
|
+
if (trimmed === '...' || trimmed === 'Ellipsis') {
|
|
333
|
+
return {
|
|
334
|
+
kind: 'callable',
|
|
335
|
+
parameters: [{ kind: 'custom', name: '...' }],
|
|
336
|
+
returnType,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const parameters = trimmed
|
|
341
|
+
? splitTopLevel(trimmed, ',').map(p => recurse(p.trim(), depth + 1))
|
|
342
|
+
: [];
|
|
343
|
+
return { kind: 'callable', parameters, returnType };
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Handles the `typing.TypeVar('T')` / `ParamSpec('P')` / `TypeVarTuple('Ts')`
|
|
347
|
+
// factory-call forms. Returns null when `rawText` is none of them, preserving
|
|
348
|
+
// the original sequential fall-through (TypeVar, then ParamSpec, then TypeVarTuple).
|
|
349
|
+
const parseTypingFactory = (rawText: string): PythonType | null => {
|
|
350
|
+
const typeVarName = parseTypingFactoryName(rawText, 'TypeVar');
|
|
351
|
+
if (typeVarName) {
|
|
352
|
+
return mapKnownTypeParameter(typeVarName) ?? { kind: 'typevar', name: typeVarName };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const paramSpecName = parseTypingFactoryName(rawText, 'ParamSpec');
|
|
356
|
+
if (paramSpecName) {
|
|
357
|
+
return mapKnownTypeParameter(paramSpecName) ?? { kind: 'paramspec', name: paramSpecName };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const typeVarTupleName = parseTypingFactoryName(rawText, 'TypeVarTuple');
|
|
361
|
+
if (typeVarTupleName) {
|
|
362
|
+
return (
|
|
363
|
+
mapKnownTypeParameter(typeVarTupleName) ?? {
|
|
364
|
+
kind: 'typevartuple',
|
|
365
|
+
name: typeVarTupleName,
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return null;
|
|
371
|
+
};
|
|
372
|
+
|
|
280
373
|
const parse = (ann: unknown, depth = 0): PythonType => {
|
|
281
374
|
if (ann === null || ann === undefined) {
|
|
282
375
|
return unknownType();
|
|
@@ -320,33 +413,18 @@ export function parseAnnotationToPythonType(
|
|
|
320
413
|
}
|
|
321
414
|
}
|
|
322
415
|
|
|
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, ',');
|
|
416
|
+
if (startsWithSpecialForm(raw, 'Union')) {
|
|
417
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
330
418
|
const types = parts.map(p => parse(p.trim(), depth + 1));
|
|
331
419
|
return { kind: 'union', types };
|
|
332
420
|
}
|
|
333
421
|
|
|
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) };
|
|
422
|
+
if (startsWithSpecialForm(raw, 'Optional')) {
|
|
423
|
+
return { kind: 'optional', type: parse(bracketInner(raw), depth + 1) };
|
|
341
424
|
}
|
|
342
425
|
|
|
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, ',');
|
|
426
|
+
if (startsWithSpecialForm(raw, 'Literal')) {
|
|
427
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
350
428
|
if (parts.length === 1) {
|
|
351
429
|
return mapLiteral(String(parts[0] ?? '').trim());
|
|
352
430
|
}
|
|
@@ -360,24 +438,9 @@ export function parseAnnotationToPythonType(
|
|
|
360
438
|
return parse(extMatch[3] ?? '', depth + 1);
|
|
361
439
|
}
|
|
362
440
|
|
|
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
|
-
);
|
|
441
|
+
const typingFactory = parseTypingFactory(rawText);
|
|
442
|
+
if (typingFactory) {
|
|
443
|
+
return typingFactory;
|
|
381
444
|
}
|
|
382
445
|
if (
|
|
383
446
|
raw === 'typing.LiteralString' ||
|
|
@@ -387,61 +450,15 @@ export function parseAnnotationToPythonType(
|
|
|
387
450
|
return { kind: 'primitive', name: 'str' };
|
|
388
451
|
}
|
|
389
452
|
|
|
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 };
|
|
453
|
+
if (startsWithSpecialForm(raw, 'Callable')) {
|
|
454
|
+
const callable = parseCallableInner(bracketInner(raw), depth, parse);
|
|
455
|
+
if (callable) {
|
|
456
|
+
return callable;
|
|
435
457
|
}
|
|
436
458
|
}
|
|
437
459
|
|
|
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, ',');
|
|
460
|
+
if (startsWithSpecialForm(raw, 'Mapping')) {
|
|
461
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
445
462
|
return {
|
|
446
463
|
kind: 'collection',
|
|
447
464
|
name: 'dict',
|
|
@@ -449,13 +466,8 @@ export function parseAnnotationToPythonType(
|
|
|
449
466
|
};
|
|
450
467
|
}
|
|
451
468
|
|
|
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, ',');
|
|
469
|
+
if (startsWithSpecialForm(raw, 'Annotated')) {
|
|
470
|
+
const parts = splitTopLevel(bracketInner(raw), ',');
|
|
459
471
|
if (parts.length > 0) {
|
|
460
472
|
return {
|
|
461
473
|
kind: 'annotated',
|
|
@@ -465,13 +477,8 @@ export function parseAnnotationToPythonType(
|
|
|
465
477
|
}
|
|
466
478
|
}
|
|
467
479
|
|
|
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) };
|
|
480
|
+
if (startsWithSpecialForm(raw, 'Unpack')) {
|
|
481
|
+
return { kind: 'unpack', type: parse(bracketInner(raw), depth + 1) };
|
|
475
482
|
}
|
|
476
483
|
|
|
477
484
|
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,6 +113,9 @@ 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) {
|
|
@@ -383,6 +412,9 @@ export class ModuleDiscovery {
|
|
|
383
412
|
* Get module version information
|
|
384
413
|
*/
|
|
385
414
|
async getModuleVersion(moduleName: string): Promise<string | undefined> {
|
|
415
|
+
// Reject names that could break out of the `python -c` expression below.
|
|
416
|
+
assertSafeModuleName(moduleName);
|
|
417
|
+
|
|
386
418
|
if (!processUtils.isAvailable()) {
|
|
387
419
|
return undefined;
|
|
388
420
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared call-emission helpers for the TypeScript wrapper generator.
|
|
3
|
+
*
|
|
4
|
+
* The function, method, and constructor wrappers all emit the same runtime
|
|
5
|
+
* prelude (assemble `__args`, fold a trailing object into `__kwargs`, unpack
|
|
6
|
+
* `*args`) and the same argument guards (reject positional-only-as-keyword,
|
|
7
|
+
* require keyword-only arguments). They differ only by indentation, the error
|
|
8
|
+
* label used in thrown messages, and the terminal RPC verb (which the call
|
|
9
|
+
* sites still emit themselves).
|
|
10
|
+
*
|
|
11
|
+
* The emitted strings MUST stay byte-identical to the previous inline copies.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Parameter } from '../types/index.js';
|
|
15
|
+
|
|
16
|
+
/** Describes one call site for prelude/guard emission. */
|
|
17
|
+
export interface CallDescriptor {
|
|
18
|
+
/** Positional parameters (no *args/**kwargs/keyword-only). */
|
|
19
|
+
positionalParams: readonly Parameter[];
|
|
20
|
+
/** The `*args` parameter, if any. */
|
|
21
|
+
varArgsParam?: Parameter;
|
|
22
|
+
/** Whether `*args` is modeled as an array param (true when kwargs coexist). */
|
|
23
|
+
needsVarArgsArray: boolean;
|
|
24
|
+
/** Whether the wrapper takes a `kwargs` parameter. */
|
|
25
|
+
hasKwArgs: boolean;
|
|
26
|
+
/** Whether the callee declares `**kwargs`. */
|
|
27
|
+
hasVarKwArgs: boolean;
|
|
28
|
+
/** Names of keyword-only parameters. */
|
|
29
|
+
keywordOnlyNames: readonly string[];
|
|
30
|
+
/** Names of required keyword-only parameters. */
|
|
31
|
+
requiredKwOnlyNames: readonly string[];
|
|
32
|
+
/** Names of positional-only parameters. */
|
|
33
|
+
positionalOnlyNames: readonly string[];
|
|
34
|
+
/** Number of leading required positional parameters. */
|
|
35
|
+
requiredPosCount: number;
|
|
36
|
+
/** Base indentation (e.g. ' ' for functions, ' ' for methods/ctors). */
|
|
37
|
+
indent: string;
|
|
38
|
+
/** Error label used in thrown messages (function/method name or '__init__'). */
|
|
39
|
+
errorLabel: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Helpers wired from the generator instance (they depend on `this`). */
|
|
43
|
+
export interface CallEmitHelpers {
|
|
44
|
+
escapeIdentifier(name: string): string;
|
|
45
|
+
renderLooksLikeKwargsExpr(
|
|
46
|
+
valueExpr: string,
|
|
47
|
+
options: {
|
|
48
|
+
keywordOnlyNames: string[];
|
|
49
|
+
requiredKwOnlyNames: string[];
|
|
50
|
+
hasVarKwArgs: boolean;
|
|
51
|
+
}
|
|
52
|
+
): string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emit the runtime prelude lines that build the `__args` array, fold a trailing
|
|
57
|
+
* plain object into `__kwargs`, and unpack `*args`.
|
|
58
|
+
*/
|
|
59
|
+
export function emitCallPrelude(desc: CallDescriptor, helpers: CallEmitHelpers): string[] {
|
|
60
|
+
const {
|
|
61
|
+
positionalParams,
|
|
62
|
+
varArgsParam,
|
|
63
|
+
needsVarArgsArray,
|
|
64
|
+
hasKwArgs,
|
|
65
|
+
hasVarKwArgs,
|
|
66
|
+
keywordOnlyNames,
|
|
67
|
+
requiredKwOnlyNames,
|
|
68
|
+
requiredPosCount,
|
|
69
|
+
indent: i,
|
|
70
|
+
errorLabel,
|
|
71
|
+
} = desc;
|
|
72
|
+
const i2 = `${i} `;
|
|
73
|
+
const i3 = `${i2} `;
|
|
74
|
+
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
if (hasKwArgs) {
|
|
77
|
+
lines.push(`${i}let __kwargs = kwargs;`);
|
|
78
|
+
}
|
|
79
|
+
const positionalArgExprs = positionalParams.map(p => helpers.escapeIdentifier(p.name));
|
|
80
|
+
lines.push(`${i}const __args: unknown[] = [${positionalArgExprs.join(', ')}];`);
|
|
81
|
+
if (requiredPosCount < positionalParams.length) {
|
|
82
|
+
lines.push(
|
|
83
|
+
`${i}while (__args.length > ${requiredPosCount} && __args[__args.length - 1] === undefined) {`
|
|
84
|
+
);
|
|
85
|
+
lines.push(`${i2}__args.pop();`);
|
|
86
|
+
lines.push(`${i}}`);
|
|
87
|
+
}
|
|
88
|
+
if (hasKwArgs && requiredPosCount < positionalParams.length) {
|
|
89
|
+
const looksLikeKwargs = helpers.renderLooksLikeKwargsExpr('__candidate', {
|
|
90
|
+
keywordOnlyNames: [...keywordOnlyNames],
|
|
91
|
+
requiredKwOnlyNames: [...requiredKwOnlyNames],
|
|
92
|
+
hasVarKwArgs,
|
|
93
|
+
});
|
|
94
|
+
lines.push(`${i}if (__kwargs === undefined && __args.length > ${requiredPosCount}) {`);
|
|
95
|
+
lines.push(`${i2}const __candidate = __args[__args.length - 1];`);
|
|
96
|
+
lines.push(`${i2}if (${looksLikeKwargs}) {`);
|
|
97
|
+
lines.push(`${i3}__kwargs = __candidate as any;`);
|
|
98
|
+
lines.push(`${i3}__args.pop();`);
|
|
99
|
+
lines.push(`${i2}}`);
|
|
100
|
+
lines.push(`${i}}`);
|
|
101
|
+
}
|
|
102
|
+
if (varArgsParam) {
|
|
103
|
+
const vname = helpers.escapeIdentifier(varArgsParam.name);
|
|
104
|
+
if (needsVarArgsArray) {
|
|
105
|
+
const looksLikeKwargs = helpers.renderLooksLikeKwargsExpr(vname, {
|
|
106
|
+
keywordOnlyNames: [...keywordOnlyNames],
|
|
107
|
+
requiredKwOnlyNames: [...requiredKwOnlyNames],
|
|
108
|
+
hasVarKwArgs,
|
|
109
|
+
});
|
|
110
|
+
lines.push(`${i}let __varargs: unknown[] = [];`);
|
|
111
|
+
lines.push(`${i}if (${vname} !== undefined) {`);
|
|
112
|
+
lines.push(`${i2}if (globalThis.Array.isArray(${vname})) {`);
|
|
113
|
+
lines.push(`${i3}__varargs = ${vname};`);
|
|
114
|
+
lines.push(`${i2}} else if (__kwargs === undefined && ${looksLikeKwargs}) {`);
|
|
115
|
+
lines.push(`${i3}__kwargs = ${vname} as any;`);
|
|
116
|
+
lines.push(`${i2}} else {`);
|
|
117
|
+
lines.push(
|
|
118
|
+
`${i3}throw new Error(\`${errorLabel} expected ${varArgsParam.name} to be an array\`);`
|
|
119
|
+
);
|
|
120
|
+
lines.push(`${i2}}`);
|
|
121
|
+
lines.push(`${i}}`);
|
|
122
|
+
lines.push(`${i}__args.push(...__varargs);`);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(`${i}__args.push(...${vname});`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Emit the argument guard lines that reject positional-only arguments passed as
|
|
132
|
+
* keywords and enforce required keyword-only arguments.
|
|
133
|
+
*/
|
|
134
|
+
export function emitArgGuards(desc: CallDescriptor): string[] {
|
|
135
|
+
const {
|
|
136
|
+
hasKwArgs,
|
|
137
|
+
positionalOnlyNames,
|
|
138
|
+
requiredKwOnlyNames,
|
|
139
|
+
indent: i,
|
|
140
|
+
errorLabel,
|
|
141
|
+
} = desc;
|
|
142
|
+
const i2 = `${i} `;
|
|
143
|
+
const i3 = `${i2} `;
|
|
144
|
+
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
if (hasKwArgs && positionalOnlyNames.length > 0) {
|
|
147
|
+
lines.push(`${i}const __positionalOnly = ${JSON.stringify(positionalOnlyNames)} as const;`);
|
|
148
|
+
lines.push(`${i}for (const key of __positionalOnly) {`);
|
|
149
|
+
lines.push(`${i2}if (__kwargs && Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
|
|
150
|
+
lines.push(
|
|
151
|
+
`${i3}throw new Error(\`${errorLabel} does not accept positional-only argument "\${key}" as a keyword argument\`);`
|
|
152
|
+
);
|
|
153
|
+
lines.push(`${i2}}`);
|
|
154
|
+
lines.push(`${i}}`);
|
|
155
|
+
}
|
|
156
|
+
if (hasKwArgs && requiredKwOnlyNames.length > 0) {
|
|
157
|
+
lines.push(`${i}const __requiredKwOnly = ${JSON.stringify(requiredKwOnlyNames)} as const;`);
|
|
158
|
+
lines.push(`${i}const __missing: string[] = [];`);
|
|
159
|
+
lines.push(`${i}for (const key of __requiredKwOnly) {`);
|
|
160
|
+
lines.push(`${i2}if (!__kwargs || !Object.prototype.hasOwnProperty.call(__kwargs, key)) {`);
|
|
161
|
+
lines.push(`${i3}__missing.push(key);`);
|
|
162
|
+
lines.push(`${i2}}`);
|
|
163
|
+
lines.push(`${i}}`);
|
|
164
|
+
lines.push(`${i}if (__missing.length > 0) {`);
|
|
165
|
+
lines.push(
|
|
166
|
+
`${i2}throw new Error(\`Missing required keyword-only arguments for ${errorLabel}: \${__missing.join(', ')}\`);`
|
|
167
|
+
);
|
|
168
|
+
lines.push(`${i}}`);
|
|
169
|
+
}
|
|
170
|
+
return lines;
|
|
171
|
+
}
|