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.
Files changed (56) hide show
  1. package/dist/core/annotation-parser.d.ts.map +1 -1
  2. package/dist/core/annotation-parser.js +19 -14
  3. package/dist/core/annotation-parser.js.map +1 -1
  4. package/dist/core/discovery.d.ts +17 -0
  5. package/dist/core/discovery.d.ts.map +1 -1
  6. package/dist/core/discovery.js +59 -49
  7. package/dist/core/discovery.js.map +1 -1
  8. package/dist/core/validation.d.ts +23 -0
  9. package/dist/core/validation.d.ts.map +1 -1
  10. package/dist/core/validation.js +52 -48
  11. package/dist/core/validation.js.map +1 -1
  12. package/dist/dev.d.ts.map +1 -1
  13. package/dist/dev.js +175 -133
  14. package/dist/dev.js.map +1 -1
  15. package/dist/runtime/bridge-codec.d.ts +15 -0
  16. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  17. package/dist/runtime/bridge-codec.js +45 -48
  18. package/dist/runtime/bridge-codec.js.map +1 -1
  19. package/dist/runtime/rpc-client.d.ts +18 -0
  20. package/dist/runtime/rpc-client.d.ts.map +1 -1
  21. package/dist/runtime/rpc-client.js +30 -24
  22. package/dist/runtime/rpc-client.js.map +1 -1
  23. package/dist/runtime/subprocess-transport.d.ts +16 -0
  24. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  25. package/dist/runtime/subprocess-transport.js +54 -35
  26. package/dist/runtime/subprocess-transport.js.map +1 -1
  27. package/dist/tywrap.d.ts +0 -5
  28. package/dist/tywrap.d.ts.map +1 -1
  29. package/dist/tywrap.js +0 -20
  30. package/dist/tywrap.js.map +1 -1
  31. package/dist/utils/cache.d.ts +11 -0
  32. package/dist/utils/cache.d.ts.map +1 -1
  33. package/dist/utils/cache.js +50 -58
  34. package/dist/utils/cache.js.map +1 -1
  35. package/dist/utils/python.d.ts.map +1 -1
  36. package/dist/utils/python.js +28 -17
  37. package/dist/utils/python.js.map +1 -1
  38. package/dist/utils/runtime.d.ts.map +1 -1
  39. package/dist/utils/runtime.js +22 -16
  40. package/dist/utils/runtime.js.map +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +1 -1
  43. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  44. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  45. package/src/core/annotation-parser.ts +23 -18
  46. package/src/core/discovery.ts +70 -54
  47. package/src/core/validation.ts +84 -48
  48. package/src/dev.ts +237 -153
  49. package/src/runtime/bridge-codec.ts +58 -70
  50. package/src/runtime/rpc-client.ts +39 -32
  51. package/src/runtime/subprocess-transport.ts +69 -39
  52. package/src/tywrap.ts +0 -24
  53. package/src/utils/cache.ts +59 -61
  54. package/src/utils/python.ts +33 -20
  55. package/src/utils/runtime.ts +24 -13
  56. 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;
@@ -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
- if (processUtils.isAvailable()) {
127
- try {
128
- const pythonPath = await this.getPythonExecutable();
129
- const result = await processUtils.exec(
130
- pythonPath,
131
- [
132
- '-c',
133
- `import ${moduleName}; print(${moduleName}.__file__ if hasattr(${moduleName}, '__file__') else '${moduleName}.__path__[0]' if hasattr(${moduleName}, '__path__') else 'builtin')`,
134
- ],
135
- { timeoutMs: this.options.timeoutMs }
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
- if (result.code === 0 && result.stdout.trim() !== 'builtin') {
139
- const modulePath = result.stdout.trim();
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
- // Cache the result
142
- this.moduleCache.set(moduleName, {
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
- return modulePath;
150
- }
151
- } catch (error) {
152
- log.warn('Failed to resolve module', { moduleName, error: String(error) });
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
- // Fallback: search in known Python paths
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 lines) {
243
- const trimmed = line.trim();
244
-
245
- // Skip comments and empty lines
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
- // Match import statements
251
- const importMatch = trimmed.match(/^import\s+([a-zA-Z_][a-zA-Z0-9_\.]*)/);
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
- // Match from ... import statements
262
- const fromMatch = trimmed.match(/^from\s+([a-zA-Z_][a-zA-Z0-9_\.]*)\s+import/);
263
- if (fromMatch?.[1]) {
264
- const fullModuleName = fromMatch[1];
265
- const moduleName = fullModuleName.split('.')[0]; // Get top-level module
266
- if (moduleName) {
267
- dependencies.push(moduleName);
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
- return [...new Set(dependencies)]; // Remove duplicates
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
  /**
@@ -195,60 +195,96 @@ export class ValidationEngine {
195
195
  errors: AnalysisError[],
196
196
  warnings: AnalysisWarning[]
197
197
  ): void {
198
- // Check return type (skip __init__ methods as they conventionally don't need return type annotations)
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
- if (this.isEmptyType(param.type) && param.name !== 'self' && param.name !== 'cls') {
221
- if (this.config.allowMissingTypeHints) {
222
- warnings.push({
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
- // Validate type structure only in strict mode (skip 'self' and 'cls' parameters)
240
- if (
241
- this.config.strictTypeChecking &&
242
- !this.config.allowMissingTypeHints &&
243
- param.name !== 'self' &&
244
- param.name !== 'cls'
245
- ) {
246
- const typeErrors = this.validateTypeAnnotation(param.type, `parameter '${param.name}'`);
247
- errors.push(...typeErrors);
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
- // Validate return type structure only in strict mode
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);