tywrap 0.2.0 → 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.
- package/README.md +65 -31
- package/dist/cli.js +49 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +43 -7
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +47 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +184 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +344 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +48 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +60 -73
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +97 -130
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +392 -521
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +1 -1
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +1 -1
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +135 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +177 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +209 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +729 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +154 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +459 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +103 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +519 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +191 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +120 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +229 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +208 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +419 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/types/index.d.ts +51 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +227 -328
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +114 -1
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +40 -18
- package/runtime/python_bridge.py +108 -15
- package/runtime/safe_codec.py +344 -0
- package/src/cli.ts +61 -2
- package/src/config/index.ts +63 -17
- package/src/core/analyzer.ts +52 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/index.ts +78 -6
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +483 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +245 -0
- package/src/runtime/http.ts +76 -115
- package/src/runtime/node.ts +563 -676
- package/src/runtime/optimized-node.ts +4 -1
- package/src/runtime/pooled-transport.ts +263 -0
- package/src/runtime/process-io.ts +930 -0
- package/src/runtime/pyodide-io.ts +551 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +651 -0
- package/src/runtime/transport.ts +278 -0
- package/src/runtime/validators.ts +252 -0
- package/src/runtime/worker-pool.ts +559 -0
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -0
- package/src/tywrap.ts +279 -358
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +122 -1
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
package/src/core/analyzer.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { getComponentLogger } from '../utils/logger.js';
|
|
|
26
26
|
const log = getComponentLogger('Analyzer');
|
|
27
27
|
|
|
28
28
|
const UNKNOWN_TYPE: PythonType = { kind: 'custom', name: 'Any', module: 'typing' };
|
|
29
|
+
// tree-sitter-python >=0.25 updates its typings, but Parser#setLanguage still expects the package export at runtime.
|
|
30
|
+
const PYTHON_LANGUAGE = Python as unknown as Parser.Language;
|
|
29
31
|
|
|
30
32
|
export class PyAnalyzer {
|
|
31
33
|
private parser: Parser;
|
|
@@ -44,7 +46,7 @@ export class PyAnalyzer {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
try {
|
|
47
|
-
await this.parser.setLanguage(
|
|
49
|
+
await this.parser.setLanguage(PYTHON_LANGUAGE);
|
|
48
50
|
this.initialized = true;
|
|
49
51
|
} catch (error) {
|
|
50
52
|
throw new Error(`Failed to initialize Python parser: ${error}`);
|
|
@@ -141,7 +143,7 @@ export class PyAnalyzer {
|
|
|
141
143
|
async extractFunctions(node: SyntaxNode): Promise<PythonFunction[]> {
|
|
142
144
|
const functions: PythonFunction[] = [];
|
|
143
145
|
|
|
144
|
-
const functionNodes = this.
|
|
146
|
+
const functionNodes = this.findDirectDefinitions(node, 'function_definition');
|
|
145
147
|
|
|
146
148
|
for (const funcNode of functionNodes) {
|
|
147
149
|
try {
|
|
@@ -161,7 +163,7 @@ export class PyAnalyzer {
|
|
|
161
163
|
async extractClasses(node: SyntaxNode): Promise<PythonClass[]> {
|
|
162
164
|
const classes: PythonClass[] = [];
|
|
163
165
|
|
|
164
|
-
const classNodes = this.
|
|
166
|
+
const classNodes = this.findDirectDefinitions(node, 'class_definition');
|
|
165
167
|
|
|
166
168
|
for (const classNode of classNodes) {
|
|
167
169
|
try {
|
|
@@ -238,7 +240,7 @@ export class PyAnalyzer {
|
|
|
238
240
|
const properties: Property[] = [];
|
|
239
241
|
|
|
240
242
|
if (bodyNode) {
|
|
241
|
-
const methodNodes = this.
|
|
243
|
+
const methodNodes = this.findDirectDefinitions(bodyNode, 'function_definition');
|
|
242
244
|
for (const methodNode of methodNodes) {
|
|
243
245
|
try {
|
|
244
246
|
const method = await this.extractFunction(methodNode);
|
|
@@ -249,9 +251,18 @@ export class PyAnalyzer {
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
// Extract properties (assignments with type annotations or @property decorators)
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
254
|
+
for (const stmt of bodyNode.namedChildren) {
|
|
255
|
+
// Only consider class-body statements, never descend into method bodies.
|
|
256
|
+
const assignmentNode =
|
|
257
|
+
stmt.type === 'assignment'
|
|
258
|
+
? stmt
|
|
259
|
+
: stmt.type === 'expression_statement'
|
|
260
|
+
? (stmt.namedChildren.find(c => c.type === 'assignment') ?? null)
|
|
261
|
+
: null;
|
|
262
|
+
if (!assignmentNode) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const prop = this.extractProperty(assignmentNode);
|
|
255
266
|
if (prop) {
|
|
256
267
|
properties.push(prop);
|
|
257
268
|
}
|
|
@@ -641,21 +652,45 @@ export class PyAnalyzer {
|
|
|
641
652
|
*/
|
|
642
653
|
private findNodesByType(node: SyntaxNode, type: string): SyntaxNode[] {
|
|
643
654
|
const results: SyntaxNode[] = [];
|
|
644
|
-
|
|
645
|
-
|
|
655
|
+
const stack: SyntaxNode[] = [node];
|
|
656
|
+
while (stack.length > 0) {
|
|
657
|
+
const n = stack.pop();
|
|
658
|
+
if (!n) {
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
646
661
|
if (n.type === type) {
|
|
647
662
|
results.push(n);
|
|
648
663
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
664
|
+
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
665
|
+
const child = n.children[i];
|
|
666
|
+
if (child) {
|
|
667
|
+
stack.push(child);
|
|
668
|
+
}
|
|
652
669
|
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
traverse(node);
|
|
670
|
+
}
|
|
656
671
|
return results;
|
|
657
672
|
}
|
|
658
673
|
|
|
674
|
+
private findDirectDefinitions(
|
|
675
|
+
node: SyntaxNode,
|
|
676
|
+
type: 'function_definition' | 'class_definition'
|
|
677
|
+
): SyntaxNode[] {
|
|
678
|
+
const out: SyntaxNode[] = [];
|
|
679
|
+
for (const child of node.namedChildren) {
|
|
680
|
+
if (child.type === type) {
|
|
681
|
+
out.push(child);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (child.type === 'decorated_definition') {
|
|
685
|
+
const def = child.namedChildren.find(c => c.type === type) ?? null;
|
|
686
|
+
if (def) {
|
|
687
|
+
out.push(def);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
|
|
659
694
|
private extractModuleName(path?: string): string {
|
|
660
695
|
if (!path) {
|
|
661
696
|
return 'unknown';
|
|
@@ -683,6 +718,8 @@ export class PyAnalyzer {
|
|
|
683
718
|
const expr = firstChild.child(0);
|
|
684
719
|
if (expr?.type === 'string') {
|
|
685
720
|
let docstring = expr.text;
|
|
721
|
+
// Strip docstring prefixes like r/u/f/b and combos (rf/fr/br/...).
|
|
722
|
+
docstring = docstring.replace(/^(?:[rRuUbBfF]{1,2})(?=(\"\"\"|'''|\"|'))/, '');
|
|
686
723
|
// Remove outer quotes and any remaining inner quotes
|
|
687
724
|
if (docstring.startsWith('"""') && docstring.endsWith('"""')) {
|
|
688
725
|
docstring = docstring.slice(3, -3);
|
|
@@ -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
|
+
}
|
package/src/core/discovery.ts
CHANGED
|
@@ -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(
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
301
|
-
|
|
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(
|
|
380
|
-
|
|
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();
|