tywrap 0.6.0 → 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/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +19 -14
- 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 +59 -49
- package/dist/core/discovery.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/runtime/bridge-codec.d.ts +15 -0
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +45 -48
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +18 -0
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +30 -24
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +16 -0
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +54 -35
- package/dist/runtime/subprocess-transport.js.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 +11 -0
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +50 -58
- package/dist/utils/cache.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.js +1 -1
- package/package.json +1 -1
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/src/core/annotation-parser.ts +23 -18
- package/src/core/discovery.ts +70 -54
- package/src/core/validation.ts +84 -48
- package/src/dev.ts +237 -153
- package/src/runtime/bridge-codec.ts +58 -70
- package/src/runtime/rpc-client.ts +39 -32
- package/src/runtime/subprocess-transport.ts +69 -39
- package/src/tywrap.ts +0 -24
- package/src/utils/cache.ts +59 -61
- package/src/utils/python.ts +33 -20
- package/src/utils/runtime.ts +24 -13
- package/src/version.ts +1 -1
|
@@ -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;
|
package/src/core/discovery.ts
CHANGED
|
@@ -122,38 +122,61 @@ export class ModuleDiscovery {
|
|
|
122
122
|
return cached.path;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// Try to resolve using Python's module finder
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
);
|
|
140
153
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
name: moduleName,
|
|
144
|
-
path: modulePath,
|
|
145
|
-
isPackage: modulePath.endsWith('__init__.py'),
|
|
146
|
-
dependencies: [],
|
|
147
|
-
});
|
|
154
|
+
if (result.code === 0 && result.stdout.trim() !== 'builtin') {
|
|
155
|
+
const modulePath = result.stdout.trim();
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
// Cache the result
|
|
158
|
+
this.moduleCache.set(moduleName, {
|
|
159
|
+
name: moduleName,
|
|
160
|
+
path: modulePath,
|
|
161
|
+
isPackage: modulePath.endsWith('__init__.py'),
|
|
162
|
+
dependencies: [],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return modulePath;
|
|
153
166
|
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
log.warn('Failed to resolve module', { moduleName, error: String(error) });
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
|
|
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> {
|
|
157
180
|
const searchPaths = await this.getPythonSearchPaths();
|
|
158
181
|
|
|
159
182
|
for (const searchPath of searchPaths) {
|
|
@@ -237,40 +260,33 @@ export class ModuleDiscovery {
|
|
|
237
260
|
*/
|
|
238
261
|
parseDependenciesFromSource(source: string): string[] {
|
|
239
262
|
const dependencies: string[] = [];
|
|
240
|
-
const lines = source.split('\n');
|
|
241
263
|
|
|
242
|
-
for (const line of
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (trimmed.startsWith('#') || trimmed === '') {
|
|
247
|
-
continue;
|
|
264
|
+
for (const line of source.split('\n')) {
|
|
265
|
+
const moduleName = this.extractImportedModule(line.trim());
|
|
266
|
+
if (moduleName) {
|
|
267
|
+
dependencies.push(moduleName);
|
|
248
268
|
}
|
|
269
|
+
}
|
|
249
270
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (importMatch?.[1]) {
|
|
253
|
-
const fullModuleName = importMatch[1];
|
|
254
|
-
const moduleName = fullModuleName.split('.')[0]; // Get top-level module
|
|
255
|
-
if (moduleName) {
|
|
256
|
-
dependencies.push(moduleName);
|
|
257
|
-
}
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
271
|
+
return [...new Set(dependencies)]; // Remove duplicates
|
|
272
|
+
}
|
|
260
273
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
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;
|
|
271
282
|
}
|
|
272
283
|
|
|
273
|
-
|
|
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
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
/**
|
package/src/core/validation.ts
CHANGED
|
@@ -195,60 +195,96 @@ export class ValidationEngine {
|
|
|
195
195
|
errors: AnalysisError[],
|
|
196
196
|
warnings: AnalysisWarning[]
|
|
197
197
|
): void {
|
|
198
|
-
|
|
199
|
-
if (this.isEmptyType(func.returnType) && func.name !== '__init__') {
|
|
200
|
-
if (this.config.allowMissingTypeHints) {
|
|
201
|
-
warnings.push({
|
|
202
|
-
type: 'missing-type',
|
|
203
|
-
message: `Function '${func.name}' is missing return type annotation`,
|
|
204
|
-
});
|
|
205
|
-
} else if (this.config.strictTypeChecking) {
|
|
206
|
-
// In strict mode without allowMissingTypeHints, we warn first then error
|
|
207
|
-
warnings.push({
|
|
208
|
-
type: 'missing-type',
|
|
209
|
-
message: `Function '${func.name}' is missing return type annotation`,
|
|
210
|
-
});
|
|
211
|
-
errors.push({
|
|
212
|
-
type: 'type',
|
|
213
|
-
message: `Function '${func.name}' requires return type annotation`,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
198
|
+
this.checkReturnTypeHint(func, errors, warnings);
|
|
217
199
|
|
|
218
200
|
// Check parameter types (skip 'self' and 'cls' parameters)
|
|
219
201
|
for (const param of func.parameters) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
type: 'missing-type',
|
|
224
|
-
message: `Parameter '${param.name}' in function '${func.name}' is missing type annotation`,
|
|
225
|
-
});
|
|
226
|
-
} else if (this.config.strictTypeChecking) {
|
|
227
|
-
// In strict mode without allowMissingTypeHints, we warn first then error
|
|
228
|
-
warnings.push({
|
|
229
|
-
type: 'missing-type',
|
|
230
|
-
message: `Parameter '${param.name}' in function '${func.name}' is missing type annotation`,
|
|
231
|
-
});
|
|
232
|
-
errors.push({
|
|
233
|
-
type: 'type',
|
|
234
|
-
message: `Parameter '${param.name}' in function '${func.name}' requires type annotation`,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
202
|
+
this.checkParameterTypeHint(func, param, errors, warnings);
|
|
203
|
+
this.checkParameterTypeStructure(param, errors);
|
|
204
|
+
}
|
|
238
205
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
206
|
+
this.checkReturnTypeStructure(func, errors);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Emit a missing-type warning, and—in strict mode—an accompanying error.
|
|
211
|
+
* In strict mode without allowMissingTypeHints, we warn first then error.
|
|
212
|
+
*/
|
|
213
|
+
private reportMissingType(
|
|
214
|
+
warningMessage: string,
|
|
215
|
+
errorMessage: string,
|
|
216
|
+
warnings: AnalysisWarning[],
|
|
217
|
+
errors: AnalysisError[]
|
|
218
|
+
): void {
|
|
219
|
+
if (this.config.allowMissingTypeHints) {
|
|
220
|
+
warnings.push({ type: 'missing-type', message: warningMessage });
|
|
221
|
+
} else if (this.config.strictTypeChecking) {
|
|
222
|
+
warnings.push({ type: 'missing-type', message: warningMessage });
|
|
223
|
+
errors.push({ type: 'type', message: errorMessage });
|
|
249
224
|
}
|
|
225
|
+
}
|
|
250
226
|
|
|
251
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Check the function return type annotation (skip __init__ methods as they
|
|
229
|
+
* conventionally don't need return type annotations).
|
|
230
|
+
*/
|
|
231
|
+
private checkReturnTypeHint(
|
|
232
|
+
func: PythonFunction,
|
|
233
|
+
errors: AnalysisError[],
|
|
234
|
+
warnings: AnalysisWarning[]
|
|
235
|
+
): void {
|
|
236
|
+
if (this.isEmptyType(func.returnType) && func.name !== '__init__') {
|
|
237
|
+
this.reportMissingType(
|
|
238
|
+
`Function '${func.name}' is missing return type annotation`,
|
|
239
|
+
`Function '${func.name}' requires return type annotation`,
|
|
240
|
+
warnings,
|
|
241
|
+
errors
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check a single parameter's type annotation (skip 'self' and 'cls').
|
|
248
|
+
*/
|
|
249
|
+
private checkParameterTypeHint(
|
|
250
|
+
func: PythonFunction,
|
|
251
|
+
param: PythonFunction['parameters'][number],
|
|
252
|
+
errors: AnalysisError[],
|
|
253
|
+
warnings: AnalysisWarning[]
|
|
254
|
+
): void {
|
|
255
|
+
if (this.isEmptyType(param.type) && param.name !== 'self' && param.name !== 'cls') {
|
|
256
|
+
this.reportMissingType(
|
|
257
|
+
`Parameter '${param.name}' in function '${func.name}' is missing type annotation`,
|
|
258
|
+
`Parameter '${param.name}' in function '${func.name}' requires type annotation`,
|
|
259
|
+
warnings,
|
|
260
|
+
errors
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate a single parameter's type structure only in strict mode
|
|
267
|
+
* (skip 'self' and 'cls' parameters).
|
|
268
|
+
*/
|
|
269
|
+
private checkParameterTypeStructure(
|
|
270
|
+
param: PythonFunction['parameters'][number],
|
|
271
|
+
errors: AnalysisError[]
|
|
272
|
+
): void {
|
|
273
|
+
if (
|
|
274
|
+
this.config.strictTypeChecking &&
|
|
275
|
+
!this.config.allowMissingTypeHints &&
|
|
276
|
+
param.name !== 'self' &&
|
|
277
|
+
param.name !== 'cls'
|
|
278
|
+
) {
|
|
279
|
+
const typeErrors = this.validateTypeAnnotation(param.type, `parameter '${param.name}'`);
|
|
280
|
+
errors.push(...typeErrors);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validate the return type structure only in strict mode.
|
|
286
|
+
*/
|
|
287
|
+
private checkReturnTypeStructure(func: PythonFunction, errors: AnalysisError[]): void {
|
|
252
288
|
if (this.config.strictTypeChecking && !this.config.allowMissingTypeHints) {
|
|
253
289
|
const returnTypeErrors = this.validateTypeAnnotation(func.returnType, 'return type');
|
|
254
290
|
errors.push(...returnTypeErrors);
|