nodepyx 1.0.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/LICENSE +22 -0
- package/README.md +399 -0
- package/binding.gyp +73 -0
- package/dist/core/PyCallable.d.ts +65 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +109 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +228 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +55 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +172 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +65 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +483 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +105 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +438 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/CondaManager.d.ts +118 -0
- package/dist/env/CondaManager.d.ts.map +1 -0
- package/dist/env/CondaManager.js +401 -0
- package/dist/env/CondaManager.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +233 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +609 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +103 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +381 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/VenvManager.d.ts +117 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +331 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/PythonTypeMapper.d.ts +87 -0
- package/dist/types/PythonTypeMapper.d.ts.map +1 -0
- package/dist/types/PythonTypeMapper.js +449 -0
- package/dist/types/PythonTypeMapper.js.map +1 -0
- package/dist/types/StubCache.d.ts +109 -0
- package/dist/types/StubCache.d.ts.map +1 -0
- package/dist/types/StubCache.js +333 -0
- package/dist/types/StubCache.js.map +1 -0
- package/dist/types/TypeGenerator.d.ts +139 -0
- package/dist/types/TypeGenerator.d.ts.map +1 -0
- package/dist/types/TypeGenerator.js +372 -0
- package/dist/types/TypeGenerator.js.map +1 -0
- package/dist/types/addon.d.ts +114 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +175 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +44 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +177 -0
- package/python/error_handler.py +433 -0
- package/python/nodepyx_runtime.py +575 -0
- package/python/serializer.py +379 -0
- package/python/type_inspector.py +288 -0
- package/scripts/build-native.js +68 -0
- package/scripts/download-prebuilds.js +99 -0
- package/scripts/generate-stubs.js +405 -0
- package/scripts/install.js +260 -0
- package/src/core/PyCallable.ts +137 -0
- package/src/core/PyContext.ts +296 -0
- package/src/core/PyIterator.ts +294 -0
- package/src/core/PyModule.ts +194 -0
- package/src/core/PyProxy.ts +605 -0
- package/src/core/PyRuntime.ts +504 -0
- package/src/env/CondaManager.ts +451 -0
- package/src/env/PackageInstaller.ts +738 -0
- package/src/env/PythonDetector.ts +414 -0
- package/src/env/VenvManager.ts +396 -0
- package/src/index.ts +425 -0
- package/src/native/gil_guard.cpp +26 -0
- package/src/native/gil_guard.h +175 -0
- package/src/native/nodepyx_addon.cpp +886 -0
- package/src/native/python_bridge.cpp +790 -0
- package/src/native/python_bridge.h +257 -0
- package/src/native/thread_pool.cpp +336 -0
- package/src/native/thread_pool.h +175 -0
- package/src/native/type_converter.cpp +901 -0
- package/src/native/type_converter.h +272 -0
- package/src/nextjs/PyProvider.tsx +123 -0
- package/src/nextjs/index.ts +21 -0
- package/src/nextjs/usePython.ts +106 -0
- package/src/nextjs/withnodepyx.ts +88 -0
- package/src/plugins/Plugin.interface.ts +51 -0
- package/src/plugins/PluginManager.ts +155 -0
- package/src/plugins/builtin/NumpyPlugin.ts +36 -0
- package/src/plugins/builtin/PandasPlugin.ts +49 -0
- package/src/plugins/builtin/TorchPlugin.ts +56 -0
- package/src/plugins/index.ts +7 -0
- package/src/serialization/DataFrameBridge.ts +398 -0
- package/src/serialization/MsgPackSerializer.ts +220 -0
- package/src/serialization/NumpyBridge.ts +332 -0
- package/src/serialization/Serializer.ts +320 -0
- package/src/types/PythonTypeMapper.ts +495 -0
- package/src/types/StubCache.ts +340 -0
- package/src/types/TypeGenerator.ts +491 -0
- package/src/types/addon.ts +170 -0
- package/src/types/config.ts +226 -0
- package/src/types/index.ts +55 -0
- package/src/types/python.ts +309 -0
- package/src/types/stubs/numpy.d.ts +441 -0
- package/src/types/stubs/pandas.d.ts +575 -0
- package/src/types/stubs/sklearn.d.ts +728 -0
- package/src/types/stubs/torch.d.ts +694 -0
- package/src/utils/ErrorTranslator.ts +220 -0
- package/src/utils/Logger.ts +119 -0
- package/src/utils/MemoryMonitor.ts +175 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — PyIterator
|
|
3
|
+
* Wraps a Python iterator for use with JavaScript's for await...of.
|
|
4
|
+
* Provides both async iteration and manual .next() calls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NativeAddon } from '../types/addon';
|
|
8
|
+
import type { PyObjectId } from '../types/python';
|
|
9
|
+
import { translatePythonError } from '../utils/ErrorTranslator';
|
|
10
|
+
import { PyProxy, PYPROXY_INTERNAL } from './PyProxy';
|
|
11
|
+
import { Logger } from '../utils/Logger';
|
|
12
|
+
|
|
13
|
+
const logger = new Logger('PyIterator');
|
|
14
|
+
|
|
15
|
+
export interface PyIteratorOptions {
|
|
16
|
+
/** Maximum number of items to yield (0 = no limit) */
|
|
17
|
+
limit?: number;
|
|
18
|
+
/** Whether to wrap Python objects in PyProxy */
|
|
19
|
+
wrapObjects?: boolean;
|
|
20
|
+
/** Timeout per .next() call (ms) */
|
|
21
|
+
timeout?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* PyIterator — manages a Python iterator lifecycle.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const iterator = await PyIterator.create(pyList, addon);
|
|
30
|
+
*
|
|
31
|
+
* for await (const item of iterator) {
|
|
32
|
+
* console.log(item);
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* // Or manually:
|
|
36
|
+
* const { value, done } = await iterator.next();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class PyIterator<T = unknown> implements AsyncIterable<T>, AsyncIterator<T> {
|
|
40
|
+
private readonly _iteratorId: number;
|
|
41
|
+
private readonly _addon: NativeAddon;
|
|
42
|
+
private readonly _objectId: PyObjectId;
|
|
43
|
+
private readonly _options: PyIteratorOptions;
|
|
44
|
+
private _done = false;
|
|
45
|
+
private _position = 0;
|
|
46
|
+
private _destroyed = false;
|
|
47
|
+
|
|
48
|
+
private constructor(
|
|
49
|
+
iteratorId: number,
|
|
50
|
+
objectId: PyObjectId,
|
|
51
|
+
addon: NativeAddon,
|
|
52
|
+
options: PyIteratorOptions = {}
|
|
53
|
+
) {
|
|
54
|
+
this._iteratorId = iteratorId;
|
|
55
|
+
this._objectId = objectId;
|
|
56
|
+
this._addon = addon;
|
|
57
|
+
this._options = options;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a PyIterator from a PyProxy.
|
|
62
|
+
*/
|
|
63
|
+
static async create(
|
|
64
|
+
source: PyProxy,
|
|
65
|
+
options?: PyIteratorOptions
|
|
66
|
+
): Promise<PyIterator> {
|
|
67
|
+
const internal = source[PYPROXY_INTERNAL];
|
|
68
|
+
const addon = internal.addon;
|
|
69
|
+
|
|
70
|
+
const result = await addon.createIterator(
|
|
71
|
+
internal.objectId as number,
|
|
72
|
+
internal.path
|
|
73
|
+
) as { success?: boolean; resultJson?: string; error?: { type: string; message: string; traceback: string } };
|
|
74
|
+
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
if (result.error) {
|
|
77
|
+
throw translatePythonError({
|
|
78
|
+
type: result.error.type,
|
|
79
|
+
message: result.error.message,
|
|
80
|
+
traceback: result.error.traceback || '',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
throw new Error('Failed to create Python iterator');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let iteratorId: number;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(result.resultJson || '{}') as { iteratorId?: number };
|
|
89
|
+
if (!parsed.iteratorId) {throw new Error('no iteratorId');}
|
|
90
|
+
iteratorId = parsed.iteratorId;
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error('Failed to parse iterator ID from: ' + result.resultJson);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return new PyIterator(iteratorId, internal.objectId, addon, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a PyIterator from an object ID and iterator ID directly.
|
|
100
|
+
*/
|
|
101
|
+
static fromIds(
|
|
102
|
+
iteratorId: number,
|
|
103
|
+
objectId: PyObjectId,
|
|
104
|
+
addon: NativeAddon,
|
|
105
|
+
options?: PyIteratorOptions
|
|
106
|
+
): PyIterator {
|
|
107
|
+
return new PyIterator(iteratorId, objectId, addon, options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Current position in iteration */
|
|
111
|
+
get position(): number {
|
|
112
|
+
return this._position;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether iteration is complete */
|
|
116
|
+
get isDone(): boolean {
|
|
117
|
+
return this._done;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── AsyncIterator Protocol ────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async next(): Promise<IteratorResult<T>> {
|
|
123
|
+
if (this._done || this._destroyed) {
|
|
124
|
+
return { value: undefined as unknown as T, done: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check limit
|
|
128
|
+
if (this._options.limit && this._position >= this._options.limit) {
|
|
129
|
+
await this.destroy();
|
|
130
|
+
return { value: undefined as unknown as T, done: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const result = await this._addon.iteratorNext(this._iteratorId) as {
|
|
135
|
+
success?: boolean;
|
|
136
|
+
resultJson?: string;
|
|
137
|
+
error?: { type: string; message: string; traceback: string };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
if (result.error) {
|
|
142
|
+
throw translatePythonError({
|
|
143
|
+
type: result.error.type,
|
|
144
|
+
message: result.error.message,
|
|
145
|
+
traceback: result.error.traceback || '',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
this._done = true;
|
|
149
|
+
return { value: undefined as unknown as T, done: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let parsed: { done?: boolean; value?: unknown };
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(result.resultJson || '{"done":true}') as { done?: boolean; value?: unknown };
|
|
155
|
+
} catch {
|
|
156
|
+
this._done = true;
|
|
157
|
+
return { value: undefined as unknown as T, done: true };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (parsed.done) {
|
|
161
|
+
this._done = true;
|
|
162
|
+
return { value: undefined as unknown as T, done: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this._position++;
|
|
166
|
+
|
|
167
|
+
// Deserialize the value
|
|
168
|
+
const rawValue = parsed.value;
|
|
169
|
+
let value: T;
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
rawValue !== null &&
|
|
173
|
+
typeof rawValue === 'object' &&
|
|
174
|
+
'__pyref__' in (rawValue as Record<string, unknown>)
|
|
175
|
+
) {
|
|
176
|
+
// It's a Python object reference
|
|
177
|
+
if (this._options.wrapObjects !== false) {
|
|
178
|
+
const refId = (rawValue as { __pyref__: number }).__pyref__;
|
|
179
|
+
value = PyProxy._createFromObjectId(
|
|
180
|
+
refId as PyObjectId,
|
|
181
|
+
[],
|
|
182
|
+
this._addon
|
|
183
|
+
) as unknown as T;
|
|
184
|
+
} else {
|
|
185
|
+
value = rawValue as T;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
value = rawValue as T;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { value, done: false };
|
|
192
|
+
} catch (err) {
|
|
193
|
+
this._done = true;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async return(value?: T): Promise<IteratorResult<T>> {
|
|
199
|
+
await this.destroy();
|
|
200
|
+
return { value: value as T, done: true };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async throw(error?: unknown): Promise<IteratorResult<T>> {
|
|
204
|
+
await this.destroy();
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── AsyncIterable Protocol ────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Utilities ─────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Collect all items into an array.
|
|
218
|
+
* Use with care for large iterators!
|
|
219
|
+
*/
|
|
220
|
+
async toArray(maxItems = 100000): Promise<T[]> {
|
|
221
|
+
const items: T[] = [];
|
|
222
|
+
for await (const item of this) {
|
|
223
|
+
items.push(item);
|
|
224
|
+
if (items.length >= maxItems) {break;}
|
|
225
|
+
}
|
|
226
|
+
return items;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Process items in batches.
|
|
231
|
+
*/
|
|
232
|
+
async *batch(batchSize: number): AsyncGenerator<T[]> {
|
|
233
|
+
let current: T[] = [];
|
|
234
|
+
for await (const item of this) {
|
|
235
|
+
current.push(item);
|
|
236
|
+
if (current.length >= batchSize) {
|
|
237
|
+
yield current;
|
|
238
|
+
current = [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (current.length > 0) {
|
|
242
|
+
yield current;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Map items through a transform function.
|
|
248
|
+
*/
|
|
249
|
+
async *map<U>(fn: (item: T, index: number) => U | Promise<U>): AsyncGenerator<U> {
|
|
250
|
+
let i = 0;
|
|
251
|
+
for await (const item of this) {
|
|
252
|
+
yield await fn(item, i++);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Filter items.
|
|
258
|
+
*/
|
|
259
|
+
async *filter(fn: (item: T, index: number) => boolean | Promise<boolean>): AsyncGenerator<T> {
|
|
260
|
+
let i = 0;
|
|
261
|
+
for await (const item of this) {
|
|
262
|
+
if (await fn(item, i++)) {yield item;}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Take first N items.
|
|
268
|
+
*/
|
|
269
|
+
async take(n: number): Promise<T[]> {
|
|
270
|
+
const items: T[] = [];
|
|
271
|
+
for await (const item of this) {
|
|
272
|
+
items.push(item);
|
|
273
|
+
if (items.length >= n) {break;}
|
|
274
|
+
}
|
|
275
|
+
await this.destroy();
|
|
276
|
+
return items;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Destroy the underlying Python iterator (frees resources).
|
|
281
|
+
* Called automatically when iteration completes.
|
|
282
|
+
*/
|
|
283
|
+
async destroy(): Promise<void> {
|
|
284
|
+
if (this._destroyed) {return;}
|
|
285
|
+
this._destroyed = true;
|
|
286
|
+
this._done = true;
|
|
287
|
+
try {
|
|
288
|
+
await this._addon.destroyIterator(this._iteratorId);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
logger.debug('Error destroying iterator', err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — PyModule
|
|
3
|
+
* Represents a Python module with its full namespace.
|
|
4
|
+
* Inherits from PyProxy but adds module-specific introspection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { PyProxy, PYPROXY_INTERNAL } from './PyProxy';
|
|
8
|
+
import type { NativeAddon } from '../types/addon';
|
|
9
|
+
import type { PyObjectId, PyModuleInspection } from '../types/python';
|
|
10
|
+
import { Logger } from '../utils/Logger';
|
|
11
|
+
|
|
12
|
+
const logger = new Logger('PyModule');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* PyModule extends PyProxy with module-specific utilities.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const pd = await py.import('pandas');
|
|
20
|
+
* // pd is a PyModule
|
|
21
|
+
* const df = await pd.read_csv('data.csv'); // access module attribute
|
|
22
|
+
* const info = await pd.__nodepyx_inspect__(); // get module metadata
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class PyModule extends PyProxy {
|
|
26
|
+
private readonly _moduleName: string;
|
|
27
|
+
private _inspection: PyModuleInspection | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
objectId: PyObjectId,
|
|
31
|
+
moduleName: string,
|
|
32
|
+
addon: NativeAddon,
|
|
33
|
+
callTimeout?: number
|
|
34
|
+
) {
|
|
35
|
+
super({
|
|
36
|
+
objectId,
|
|
37
|
+
path: [],
|
|
38
|
+
addon,
|
|
39
|
+
isResolved: false,
|
|
40
|
+
callTimeout,
|
|
41
|
+
childCache: new Map(),
|
|
42
|
+
});
|
|
43
|
+
this._moduleName = moduleName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The Python module name */
|
|
47
|
+
get moduleName(): string {
|
|
48
|
+
return this._moduleName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Object ID in Python heap */
|
|
52
|
+
get objectId(): PyObjectId {
|
|
53
|
+
return this[PYPROXY_INTERNAL].objectId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inspect the module — returns detailed type information.
|
|
58
|
+
* Cached after first call.
|
|
59
|
+
*/
|
|
60
|
+
async inspect(): Promise<PyModuleInspection> {
|
|
61
|
+
if (this._inspection) {return this._inspection;}
|
|
62
|
+
|
|
63
|
+
const addon = this[PYPROXY_INTERNAL].addon;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Run Python introspection
|
|
67
|
+
const result = await addon.runPythonCode(`
|
|
68
|
+
import json
|
|
69
|
+
import sys
|
|
70
|
+
|
|
71
|
+
def _inspect_module_${this._moduleName.replace(/\./g, '_')}():
|
|
72
|
+
try:
|
|
73
|
+
import ${this._moduleName} as _mod
|
|
74
|
+
import inspect
|
|
75
|
+
|
|
76
|
+
result = {
|
|
77
|
+
'name': '${this._moduleName}',
|
|
78
|
+
'file': getattr(_mod, '__file__', None),
|
|
79
|
+
'docstring': (getattr(_mod, '__doc__', '') or '')[:500],
|
|
80
|
+
'version': getattr(_mod, '__version__', None),
|
|
81
|
+
'functions': [],
|
|
82
|
+
'classes': [],
|
|
83
|
+
'constants': [],
|
|
84
|
+
'submodules': [],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for name in dir(_mod):
|
|
88
|
+
if name.startswith('_'):
|
|
89
|
+
continue
|
|
90
|
+
try:
|
|
91
|
+
obj = getattr(_mod, name)
|
|
92
|
+
if inspect.ismodule(obj):
|
|
93
|
+
result['submodules'].append(name)
|
|
94
|
+
elif inspect.isclass(obj):
|
|
95
|
+
result['classes'].append({'name': name, 'type': 'class'})
|
|
96
|
+
elif callable(obj):
|
|
97
|
+
result['functions'].append({'name': name, 'type': 'function'})
|
|
98
|
+
else:
|
|
99
|
+
result['constants'].append({'name': name, 'type': type(obj).__name__})
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {'name': '${this._moduleName}', 'error': str(e), 'functions': [], 'classes': [], 'constants': [], 'submodules': []}
|
|
106
|
+
|
|
107
|
+
_result = _inspect_module_${this._moduleName.replace(/\./g, '_')}()
|
|
108
|
+
json.dumps(_result)
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
const r = result as { success?: boolean; resultJson?: string };
|
|
112
|
+
if (r.success && r.resultJson) {
|
|
113
|
+
const parsed = JSON.parse(r.resultJson.replace(/^"|"$/g, '').replace(/\\"/g, '"'));
|
|
114
|
+
this._inspection = parsed as PyModuleInspection;
|
|
115
|
+
return this._inspection;
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
logger.warn(`Failed to inspect module ${this._moduleName}`, err);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Return minimal inspection
|
|
122
|
+
this._inspection = {
|
|
123
|
+
name: this._moduleName,
|
|
124
|
+
docstring: '',
|
|
125
|
+
functions: [],
|
|
126
|
+
classes: [],
|
|
127
|
+
constants: [],
|
|
128
|
+
submodules: [],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return this._inspection;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get list of public names in the module (dir() equivalent).
|
|
136
|
+
*/
|
|
137
|
+
async getNames(): Promise<string[]> {
|
|
138
|
+
const addon = this[PYPROXY_INTERNAL].addon;
|
|
139
|
+
const result = await addon.getObjectKeys(this.objectId as number);
|
|
140
|
+
const r = result as { success?: boolean; resultJson?: string };
|
|
141
|
+
if (r.success && r.resultJson) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(r.resultJson) as string[];
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if the module has a specific attribute.
|
|
153
|
+
*/
|
|
154
|
+
async has(name: string): Promise<boolean> {
|
|
155
|
+
const addon = this[PYPROXY_INTERNAL].addon;
|
|
156
|
+
return addon.hasAttribute(this.objectId as number, name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get module version string.
|
|
161
|
+
*/
|
|
162
|
+
async getVersion(): Promise<string | null> {
|
|
163
|
+
const inspection = await this.inspect();
|
|
164
|
+
return inspection.version ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Reload the module (re-imports from Python).
|
|
169
|
+
*/
|
|
170
|
+
async reload(): Promise<void> {
|
|
171
|
+
const addon = this[PYPROXY_INTERNAL].addon;
|
|
172
|
+
await addon.reloadModule(this.objectId as number);
|
|
173
|
+
this._inspection = null; // Clear inspection cache
|
|
174
|
+
this[PYPROXY_INTERNAL].childCache.clear(); // Clear attribute cache
|
|
175
|
+
logger.debug(`Module ${this._moduleName} reloaded`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a PyModule from an existing addon import result.
|
|
180
|
+
*/
|
|
181
|
+
static fromRef(
|
|
182
|
+
objectId: PyObjectId,
|
|
183
|
+
moduleName: string,
|
|
184
|
+
addon: NativeAddon,
|
|
185
|
+
callTimeout?: number
|
|
186
|
+
): PyModule {
|
|
187
|
+
return new PyModule(objectId, moduleName, addon, callTimeout);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override toString(): string {
|
|
191
|
+
return `[PyModule '${this._moduleName}' id=${this.objectId}]`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|