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,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* nodepyx — Post-install Script
|
|
4
|
+
* Tries to load pre-built binaries, falls back to building from source.
|
|
5
|
+
* Runs automatically via `postinstall` npm hook.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { execSync, spawnSync } = require('child_process');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
16
|
+
const SKIP_BUILD = process.env.nodepyx_SKIP_BUILD === '1' || process.env.CI_SKIP_NATIVE === '1';
|
|
17
|
+
const FORCE_BUILD = process.env.nodepyx_FORCE_BUILD === '1';
|
|
18
|
+
const VERBOSE = process.env.nodepyx_VERBOSE === '1' || process.env.npm_config_loglevel === 'verbose';
|
|
19
|
+
|
|
20
|
+
function log(msg) {
|
|
21
|
+
console.log(`[nodepyx install] ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function logVerbose(msg) {
|
|
25
|
+
if (VERBOSE) log(msg);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function warn(msg) {
|
|
29
|
+
console.warn(`[nodepyx install] WARNING: ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function error(msg) {
|
|
33
|
+
console.error(`[nodepyx install] ERROR: ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Get platform string matching prebuilds directory naming */
|
|
37
|
+
function getPlatformDir() {
|
|
38
|
+
const platform = process.platform;
|
|
39
|
+
const arch = process.arch;
|
|
40
|
+
return `${platform}-${arch}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Try to load the addon to verify it works */
|
|
44
|
+
function tryLoad(addonPath) {
|
|
45
|
+
try {
|
|
46
|
+
require(addonPath);
|
|
47
|
+
return true;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
logVerbose(`Failed to load ${addonPath}: ${e.message}`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Try to find and load a pre-built binary */
|
|
55
|
+
function tryPrebuilt() {
|
|
56
|
+
const platformDir = getPlatformDir();
|
|
57
|
+
const prebuildsDir = path.join(ROOT, 'prebuilds', platformDir);
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(prebuildsDir)) {
|
|
60
|
+
logVerbose(`No prebuilds directory for ${platformDir}`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Look for .node files in the prebuilds directory
|
|
65
|
+
const files = fs.readdirSync(prebuildsDir).filter(f => f.endsWith('.node'));
|
|
66
|
+
|
|
67
|
+
if (files.length === 0) {
|
|
68
|
+
logVerbose(`No .node files in ${prebuildsDir}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
const addonPath = path.join(prebuildsDir, file);
|
|
74
|
+
if (tryLoad(addonPath)) {
|
|
75
|
+
log(`Loaded pre-built binary: ${path.relative(ROOT, addonPath)}`);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Try to load from build/Release */
|
|
84
|
+
function tryBuildRelease() {
|
|
85
|
+
const releasePath = path.join(ROOT, 'build', 'Release', 'nodepyx_addon.node');
|
|
86
|
+
if (fs.existsSync(releasePath) && tryLoad(releasePath)) {
|
|
87
|
+
log('Loaded from build/Release');
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Check if build tools are available */
|
|
94
|
+
function hasBuildTools() {
|
|
95
|
+
try {
|
|
96
|
+
execSync('node-gyp --version', { stdio: 'ignore' });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
try {
|
|
100
|
+
// Try via npx
|
|
101
|
+
execSync('npx node-gyp --version', { stdio: 'ignore' });
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Check if Python development headers are available */
|
|
110
|
+
function hasPythonDev() {
|
|
111
|
+
const pythons = ['python3', 'python'];
|
|
112
|
+
for (const py of pythons) {
|
|
113
|
+
try {
|
|
114
|
+
const result = execSync(`${py}-config --includes 2>/dev/null || ${py} -c "import sysconfig; print(sysconfig.get_path('include'))"`, {
|
|
115
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
116
|
+
timeout: 5000,
|
|
117
|
+
});
|
|
118
|
+
const includes = result.toString().trim();
|
|
119
|
+
if (includes) {
|
|
120
|
+
logVerbose(`Python dev headers found via: ${py}`);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// continue
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Build the native addon from source */
|
|
131
|
+
function buildFromSource() {
|
|
132
|
+
log('Building native addon from source...');
|
|
133
|
+
log('This may take a few minutes...');
|
|
134
|
+
|
|
135
|
+
const nodeGyp = 'node-gyp';
|
|
136
|
+
const buildArgs = ['rebuild', '--release'];
|
|
137
|
+
|
|
138
|
+
if (VERBOSE) {
|
|
139
|
+
buildArgs.push('--verbose');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = spawnSync(nodeGyp, buildArgs, {
|
|
143
|
+
cwd: ROOT,
|
|
144
|
+
stdio: 'inherit',
|
|
145
|
+
env: { ...process.env },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (result.status !== 0) {
|
|
149
|
+
// Try via npx
|
|
150
|
+
const result2 = spawnSync('npx', [nodeGyp, ...buildArgs], {
|
|
151
|
+
cwd: ROOT,
|
|
152
|
+
stdio: 'inherit',
|
|
153
|
+
env: { ...process.env },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (result2.status !== 0) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return tryBuildRelease();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Print helpful error message with install instructions */
|
|
165
|
+
function printInstallHelp() {
|
|
166
|
+
const platform = process.platform;
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log('┌─────────────────────────────────────────────────────────────────â”');
|
|
170
|
+
console.log('│ nodepyx install failed │');
|
|
171
|
+
console.log('└─────────────────────────────────────────────────────────────────┘');
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log('nodepyx requires:');
|
|
174
|
+
console.log(' • Python 3.8+ with development headers');
|
|
175
|
+
console.log(' • C++ compiler (GCC/Clang/MSVC)');
|
|
176
|
+
console.log(' • node-gyp build tools');
|
|
177
|
+
console.log('');
|
|
178
|
+
|
|
179
|
+
if (platform === 'linux') {
|
|
180
|
+
console.log('Ubuntu/Debian install:');
|
|
181
|
+
console.log(' sudo apt-get install python3-dev build-essential');
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log('RHEL/CentOS/Fedora install:');
|
|
184
|
+
console.log(' sudo dnf install python3-devel gcc-c++ make');
|
|
185
|
+
} else if (platform === 'darwin') {
|
|
186
|
+
console.log('macOS install:');
|
|
187
|
+
console.log(' brew install python3');
|
|
188
|
+
console.log(' xcode-select --install');
|
|
189
|
+
} else if (platform === 'win32') {
|
|
190
|
+
console.log('Windows install:');
|
|
191
|
+
console.log(' npm install --global windows-build-tools');
|
|
192
|
+
console.log(' or install Visual Studio Build Tools');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log('To skip native build (limited functionality):');
|
|
197
|
+
console.log(' nodepyx_SKIP_BUILD=1 npm install nodepyx');
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log('See https://nodepyx.dev/install for detailed instructions.');
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function main() {
|
|
204
|
+
log(`Installing nodepyx native addon for ${process.platform}-${process.arch} / Node.js ${process.version}`);
|
|
205
|
+
|
|
206
|
+
if (SKIP_BUILD) {
|
|
207
|
+
warn('nodepyx_SKIP_BUILD=1 — skipping native addon build. Functionality will be limited.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 1. Try pre-built binary (fastest)
|
|
212
|
+
if (!FORCE_BUILD && tryPrebuilt()) {
|
|
213
|
+
log('✓ Pre-built binary loaded successfully');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 2. Try existing build
|
|
218
|
+
if (!FORCE_BUILD && tryBuildRelease()) {
|
|
219
|
+
log('✓ Existing build loaded successfully');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 3. Try to build from source
|
|
224
|
+
if (!hasBuildTools()) {
|
|
225
|
+
warn('node-gyp not found. Cannot build from source.');
|
|
226
|
+
printInstallHelp();
|
|
227
|
+
|
|
228
|
+
if (process.env.nodepyx_REQUIRE_NATIVE === '1') {
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!hasPythonDev()) {
|
|
235
|
+
warn('Python development headers not found. Cannot build native addon.');
|
|
236
|
+
printInstallHelp();
|
|
237
|
+
|
|
238
|
+
if (process.env.nodepyx_REQUIRE_NATIVE === '1') {
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (buildFromSource()) {
|
|
245
|
+
log('✓ Native addon built and loaded successfully');
|
|
246
|
+
} else {
|
|
247
|
+
error('Failed to build native addon.');
|
|
248
|
+
printInstallHelp();
|
|
249
|
+
|
|
250
|
+
if (process.env.nodepyx_REQUIRE_NATIVE === '1') {
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
main().catch(err => {
|
|
257
|
+
error(`Unexpected error: ${err.message}`);
|
|
258
|
+
process.exit(0); // Don't fail npm install
|
|
259
|
+
});
|
|
260
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — PyCallable
|
|
3
|
+
* Represents a Python callable (function, method, class constructor).
|
|
4
|
+
* Extends PyProxy with call metadata and convenience methods.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { PyProxy, PYPROXY_INTERNAL } from './PyProxy';
|
|
8
|
+
import type { NativeAddon } from '../types/addon';
|
|
9
|
+
import type { PyObjectId, AttributePath, PyCallableInfo } from '../types/python';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PyCallable — wraps a Python function or method.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const np = await py.import('numpy');
|
|
17
|
+
* const zeros = await np.zeros; // PyCallable
|
|
18
|
+
* const arr = await zeros([3, 3], { dtype: 'float64' });
|
|
19
|
+
*
|
|
20
|
+
* // Or more concisely:
|
|
21
|
+
* const arr2 = await np.zeros([3, 3]);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class PyCallable extends PyProxy {
|
|
25
|
+
private readonly _callableName: string;
|
|
26
|
+
private _info: PyCallableInfo | null = null;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
objectId: PyObjectId,
|
|
30
|
+
path: AttributePath,
|
|
31
|
+
callableName: 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._callableName = callableName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Callable name */
|
|
47
|
+
get callableName(): string {
|
|
48
|
+
return this._callableName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Call the callable with given arguments.
|
|
53
|
+
* Same as using the callable directly (proxy syntax).
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const result = await callable.invoke([1, 2, 3], { axis: 0 });
|
|
58
|
+
* // equivalent to: await callable([1, 2, 3], { axis: 0 })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async invoke(...args: unknown[]): Promise<unknown> {
|
|
62
|
+
return (this as unknown as (...a: unknown[]) => unknown)(...args);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Call with explicit positional args and keyword args.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const result = await callable.call([1, 2, 3], { axis: 0, keepdims: true });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
async call(args: unknown[], kwargs?: Record<string, unknown>): Promise<unknown> {
|
|
74
|
+
if (kwargs) {
|
|
75
|
+
return (this as unknown as (...a: unknown[]) => unknown)(...args, kwargs);
|
|
76
|
+
}
|
|
77
|
+
return (this as unknown as (...a: unknown[]) => unknown)(...args);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Partially apply arguments (like functools.partial in Python).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const readCsvUtf8 = await pd.read_csv.partial({ encoding: 'utf-8' });
|
|
86
|
+
* const df = await readCsvUtf8('data.csv');
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
partial(...partialArgs: unknown[]): PyCallable {
|
|
90
|
+
const internal = this[PYPROXY_INTERNAL];
|
|
91
|
+
const callableName = `${this._callableName}(partial)`;
|
|
92
|
+
const wrapper = new PyCallable(
|
|
93
|
+
internal.objectId,
|
|
94
|
+
internal.path,
|
|
95
|
+
callableName,
|
|
96
|
+
internal.addon,
|
|
97
|
+
internal.callTimeout
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Override: always prepend partialArgs (use arrow to capture outer `this` lexically)
|
|
101
|
+
const callSelf = (...a: unknown[]) =>
|
|
102
|
+
(this as unknown as (...x: unknown[]) => unknown)(...a);
|
|
103
|
+
const overrideProxy = new Proxy(wrapper, {
|
|
104
|
+
apply(_target, _thisArg, args: unknown[]) {
|
|
105
|
+
return callSelf(...partialArgs, ...args);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return overrideProxy as PyCallable;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get introspection information about this callable.
|
|
114
|
+
*/
|
|
115
|
+
async getInfo(): Promise<PyCallableInfo | null> {
|
|
116
|
+
if (this._info) {return this._info;}
|
|
117
|
+
|
|
118
|
+
// Not yet implemented — would require Python introspection
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static fromProxy(proxy: PyProxy, name: string): PyCallable {
|
|
123
|
+
const internal = proxy[PYPROXY_INTERNAL];
|
|
124
|
+
return new PyCallable(
|
|
125
|
+
internal.objectId,
|
|
126
|
+
internal.path,
|
|
127
|
+
name,
|
|
128
|
+
internal.addon,
|
|
129
|
+
internal.callTimeout
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override toString(): string {
|
|
134
|
+
return `[PyCallable '${this._callableName}' id=${this[PYPROXY_INTERNAL].objectId}]`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — PyContext
|
|
3
|
+
* Manages an isolated Python execution context (namespace).
|
|
4
|
+
* Useful for running code in isolation without polluting the global namespace.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NativeAddon } from '../types/addon';
|
|
8
|
+
import type { PyObjectId } from '../types/python';
|
|
9
|
+
import { PyProxy, PYPROXY_INTERNAL } from './PyProxy';
|
|
10
|
+
import { translatePythonError } from '../utils/ErrorTranslator';
|
|
11
|
+
import { Logger } from '../utils/Logger';
|
|
12
|
+
|
|
13
|
+
const logger = new Logger('PyContext');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* PyContext — an isolated Python execution environment.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const ctx = await PyContext.create(addon);
|
|
21
|
+
*
|
|
22
|
+
* // Run code in isolation
|
|
23
|
+
* await ctx.exec('x = 42');
|
|
24
|
+
* await ctx.exec('y = x * 2');
|
|
25
|
+
* const result = await ctx.eval('x + y');
|
|
26
|
+
* console.log(result); // 126
|
|
27
|
+
*
|
|
28
|
+
* // Import into context
|
|
29
|
+
* const pd = await ctx.import('pandas');
|
|
30
|
+
* await ctx.exec("df = pd.DataFrame({'a': [1,2,3]})");
|
|
31
|
+
* const df = await ctx.get('df');
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class PyContext {
|
|
35
|
+
private readonly _addon: NativeAddon;
|
|
36
|
+
private readonly _namespacePyId: PyObjectId;
|
|
37
|
+
private _variables: Map<string, PyObjectId>;
|
|
38
|
+
private _destroyed = false;
|
|
39
|
+
|
|
40
|
+
private constructor(addon: NativeAddon, namespacePyId: PyObjectId) {
|
|
41
|
+
this._addon = addon;
|
|
42
|
+
this._namespacePyId = namespacePyId;
|
|
43
|
+
this._variables = new Map();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a new isolated Python context.
|
|
48
|
+
*/
|
|
49
|
+
static async create(addon: NativeAddon): Promise<PyContext> {
|
|
50
|
+
// Create a fresh namespace dict in Python
|
|
51
|
+
const result = await addon.evalPython('{}') as {
|
|
52
|
+
success?: boolean;
|
|
53
|
+
objectId?: number;
|
|
54
|
+
isObject?: boolean;
|
|
55
|
+
error?: { type: string; message: string; traceback: string };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
if (result.error) {
|
|
60
|
+
throw translatePythonError({
|
|
61
|
+
type: result.error.type,
|
|
62
|
+
message: result.error.message,
|
|
63
|
+
traceback: result.error.traceback || '',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
throw new Error('Failed to create Python context');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!result.objectId) {
|
|
70
|
+
throw new Error('Context namespace object ID not returned');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new PyContext(addon, result.objectId as PyObjectId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute Python code in this context.
|
|
78
|
+
*/
|
|
79
|
+
async exec(code: string): Promise<void> {
|
|
80
|
+
this._ensureNotDestroyed();
|
|
81
|
+
|
|
82
|
+
const result = await this._addon.runPythonCode(code) as {
|
|
83
|
+
success?: boolean;
|
|
84
|
+
error?: { type: string; message: string; traceback: string };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!result.success && result.error) {
|
|
88
|
+
throw translatePythonError({
|
|
89
|
+
type: result.error.type,
|
|
90
|
+
message: result.error.message,
|
|
91
|
+
traceback: result.error.traceback || '',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Evaluate a Python expression in this context and return the result.
|
|
98
|
+
*/
|
|
99
|
+
async eval(expression: string): Promise<unknown> {
|
|
100
|
+
this._ensureNotDestroyed();
|
|
101
|
+
|
|
102
|
+
const result = await this._addon.evalPython(expression) as {
|
|
103
|
+
success?: boolean;
|
|
104
|
+
resultJson?: string;
|
|
105
|
+
objectId?: number;
|
|
106
|
+
isObject?: boolean;
|
|
107
|
+
error?: { type: string; message: string; traceback: string };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
if (result.error) {
|
|
112
|
+
throw translatePythonError({
|
|
113
|
+
type: result.error.type,
|
|
114
|
+
message: result.error.message,
|
|
115
|
+
traceback: result.error.traceback || '',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
throw new Error('Evaluation failed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result.isObject && result.objectId) {
|
|
122
|
+
return PyProxy._createFromObjectId(
|
|
123
|
+
result.objectId as PyObjectId,
|
|
124
|
+
[],
|
|
125
|
+
this._addon
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (result.resultJson !== undefined) {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(result.resultJson);
|
|
132
|
+
} catch {
|
|
133
|
+
return result.resultJson;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Import a module into this context.
|
|
142
|
+
*/
|
|
143
|
+
async import(moduleName: string): Promise<PyProxy> {
|
|
144
|
+
this._ensureNotDestroyed();
|
|
145
|
+
|
|
146
|
+
const result = await this._addon.importModule(moduleName) as {
|
|
147
|
+
success?: boolean;
|
|
148
|
+
objectId?: number;
|
|
149
|
+
error?: { type: string; message: string; traceback: string };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (!result.success) {
|
|
153
|
+
if (result.error) {
|
|
154
|
+
throw translatePythonError({
|
|
155
|
+
type: result.error.type,
|
|
156
|
+
message: result.error.message,
|
|
157
|
+
traceback: result.error.traceback || '',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`Failed to import module '${moduleName}'`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!result.objectId) {
|
|
164
|
+
throw new Error('Module object ID not returned');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return PyProxy._createFromObjectId(
|
|
168
|
+
result.objectId as PyObjectId,
|
|
169
|
+
[],
|
|
170
|
+
this._addon
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a variable from the context namespace.
|
|
176
|
+
*/
|
|
177
|
+
async get(name: string): Promise<unknown> {
|
|
178
|
+
this._ensureNotDestroyed();
|
|
179
|
+
|
|
180
|
+
const pyId = this._variables.get(name);
|
|
181
|
+
|
|
182
|
+
if (pyId) {
|
|
183
|
+
return PyProxy._createFromObjectId(pyId, [], this._addon);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Try to get from Python global namespace
|
|
187
|
+
const result = await this._addon.evalPython(name) as {
|
|
188
|
+
success?: boolean;
|
|
189
|
+
resultJson?: string;
|
|
190
|
+
objectId?: number;
|
|
191
|
+
isObject?: boolean;
|
|
192
|
+
error?: { type: string; message: string; traceback: string };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (!result.success) {
|
|
196
|
+
if (result.error?.type === 'NameError') {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
if (result.error) {
|
|
200
|
+
throw translatePythonError({
|
|
201
|
+
type: result.error.type,
|
|
202
|
+
message: result.error.message,
|
|
203
|
+
traceback: result.error.traceback || '',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (result.isObject && result.objectId) {
|
|
210
|
+
return PyProxy._createFromObjectId(
|
|
211
|
+
result.objectId as PyObjectId,
|
|
212
|
+
[],
|
|
213
|
+
this._addon
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (result.resultJson !== undefined) {
|
|
218
|
+
try {
|
|
219
|
+
return JSON.parse(result.resultJson);
|
|
220
|
+
} catch {
|
|
221
|
+
return result.resultJson;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Set a variable in the context namespace.
|
|
230
|
+
*/
|
|
231
|
+
async set(name: string, value: unknown): Promise<void> {
|
|
232
|
+
this._ensureNotDestroyed();
|
|
233
|
+
|
|
234
|
+
if (value instanceof PyProxy) {
|
|
235
|
+
const internal = value[PYPROXY_INTERNAL];
|
|
236
|
+
this._variables.set(name, internal.objectId);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// For primitives and serializable values: set via exec
|
|
241
|
+
const serialized = JSON.stringify(value);
|
|
242
|
+
await this.exec(`${name} = __import__('json').loads('${serialized.replace(/'/g, "\\'")}')`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a name is defined in the context.
|
|
247
|
+
*/
|
|
248
|
+
async has(name: string): Promise<boolean> {
|
|
249
|
+
this._ensureNotDestroyed();
|
|
250
|
+
try {
|
|
251
|
+
const val = await this.get(name);
|
|
252
|
+
return val !== undefined;
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Delete a variable from the context.
|
|
260
|
+
*/
|
|
261
|
+
async delete(name: string): Promise<void> {
|
|
262
|
+
this._ensureNotDestroyed();
|
|
263
|
+
this._variables.delete(name);
|
|
264
|
+
await this.exec(`del ${name}`).catch(() => {}); // ignore if not defined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Clear all variables in the context.
|
|
269
|
+
*/
|
|
270
|
+
async clear(): Promise<void> {
|
|
271
|
+
this._ensureNotDestroyed();
|
|
272
|
+
this._variables.clear();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Destroy the context and free resources.
|
|
277
|
+
*/
|
|
278
|
+
async destroy(): Promise<void> {
|
|
279
|
+
if (this._destroyed) {return;}
|
|
280
|
+
this._destroyed = true;
|
|
281
|
+
this._variables.clear();
|
|
282
|
+
this._addon.decRef(this._namespacePyId as number);
|
|
283
|
+
logger.debug('PyContext destroyed');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private _ensureNotDestroyed(): void {
|
|
287
|
+
if (this._destroyed) {
|
|
288
|
+
throw new Error('PyContext has been destroyed');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
[Symbol.asyncDispose](): Promise<void> {
|
|
293
|
+
return this.destroy();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|