pynect 0.0.0 → 0.2.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/LICENSE +1 -1
- package/README.md +503 -2
- package/SECURITY.md +34 -0
- package/binding.gyp +67 -0
- package/dist/cli/codegen.d.ts +3 -0
- package/dist/cli/codegen.d.ts.map +1 -0
- package/dist/cli/codegen.js +543 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/sync.d.ts +3 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +47 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/project.d.ts +113 -0
- package/dist/config/project.d.ts.map +1 -0
- package/dist/config/project.js +344 -0
- package/dist/config/project.js.map +1 -0
- package/dist/core/isolated-worker.d.ts +39 -0
- package/dist/core/isolated-worker.d.ts.map +1 -0
- package/dist/core/isolated-worker.js +178 -0
- package/dist/core/isolated-worker.js.map +1 -0
- package/dist/core/py-callable.d.ts +67 -0
- package/dist/core/py-callable.d.ts.map +1 -0
- package/dist/core/py-callable.js +104 -0
- package/dist/core/py-callable.js.map +1 -0
- package/dist/core/py-context.d.ts +76 -0
- package/dist/core/py-context.d.ts.map +1 -0
- package/dist/core/py-context.js +239 -0
- package/dist/core/py-context.js.map +1 -0
- package/dist/core/py-iterator.d.ts +84 -0
- package/dist/core/py-iterator.d.ts.map +1 -0
- package/dist/core/py-iterator.js +243 -0
- package/dist/core/py-iterator.js.map +1 -0
- package/dist/core/py-module.d.ts +53 -0
- package/dist/core/py-module.d.ts.map +1 -0
- package/dist/core/py-module.js +162 -0
- package/dist/core/py-module.js.map +1 -0
- package/dist/core/py-proxy.d.ts +75 -0
- package/dist/core/py-proxy.d.ts.map +1 -0
- package/dist/core/py-proxy.js +563 -0
- package/dist/core/py-proxy.js.map +1 -0
- package/dist/core/py-runtime.d.ts +113 -0
- package/dist/core/py-runtime.d.ts.map +1 -0
- package/dist/core/py-runtime.js +528 -0
- package/dist/core/py-runtime.js.map +1 -0
- package/dist/env/executables.d.ts +3 -0
- package/dist/env/executables.d.ts.map +1 -0
- package/dist/env/executables.js +14 -0
- package/dist/env/executables.js.map +1 -0
- package/dist/env/package-installer.d.ts +232 -0
- package/dist/env/package-installer.d.ts.map +1 -0
- package/dist/env/package-installer.js +615 -0
- package/dist/env/package-installer.js.map +1 -0
- package/dist/env/project-environment.d.ts +64 -0
- package/dist/env/project-environment.d.ts.map +1 -0
- package/dist/env/project-environment.js +388 -0
- package/dist/env/project-environment.js.map +1 -0
- package/dist/env/pyfig-adapter.d.ts +38 -0
- package/dist/env/pyfig-adapter.d.ts.map +1 -0
- package/dist/env/pyfig-adapter.js +10 -0
- package/dist/env/pyfig-adapter.js.map +1 -0
- package/dist/env/python-detector.d.ts +48 -0
- package/dist/env/python-detector.d.ts.map +1 -0
- package/dist/env/python-detector.js +182 -0
- package/dist/env/python-detector.js.map +1 -0
- package/dist/env/python-provisioner.d.ts +16 -0
- package/dist/env/python-provisioner.d.ts.map +1 -0
- package/dist/env/python-provisioner.js +42 -0
- package/dist/env/python-provisioner.js.map +1 -0
- package/dist/env/venv-manager.d.ts +56 -0
- package/dist/env/venv-manager.d.ts.map +1 -0
- package/dist/env/venv-manager.js +160 -0
- package/dist/env/venv-manager.js.map +1 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +394 -0
- package/dist/index.js.map +1 -0
- package/dist/native/native-addon-loader.d.ts +13 -0
- package/dist/native/native-addon-loader.d.ts.map +1 -0
- package/dist/native/native-addon-loader.js +171 -0
- package/dist/native/native-addon-loader.js.map +1 -0
- package/dist/plugins/builtin/numpy-plugin.d.ts +17 -0
- package/dist/plugins/builtin/numpy-plugin.d.ts.map +1 -0
- package/dist/plugins/builtin/numpy-plugin.js +41 -0
- package/dist/plugins/builtin/numpy-plugin.js.map +1 -0
- package/dist/plugins/builtin/pandas-plugin.d.ts +19 -0
- package/dist/plugins/builtin/pandas-plugin.d.ts.map +1 -0
- package/dist/plugins/builtin/pandas-plugin.js +57 -0
- package/dist/plugins/builtin/pandas-plugin.js.map +1 -0
- package/dist/plugins/builtin/torch-plugin.d.ts +23 -0
- package/dist/plugins/builtin/torch-plugin.d.ts.map +1 -0
- package/dist/plugins/builtin/torch-plugin.js +50 -0
- package/dist/plugins/builtin/torch-plugin.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/plugins/plugin-manager.d.ts +26 -0
- package/dist/plugins/plugin-manager.d.ts.map +1 -0
- package/dist/plugins/plugin-manager.js +174 -0
- package/dist/plugins/plugin-manager.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/serialization/data-frame-bridge.d.ts +141 -0
- package/dist/serialization/data-frame-bridge.d.ts.map +1 -0
- package/dist/serialization/data-frame-bridge.js +355 -0
- package/dist/serialization/data-frame-bridge.js.map +1 -0
- package/dist/serialization/msg-pack-serializer.d.ts +45 -0
- package/dist/serialization/msg-pack-serializer.d.ts.map +1 -0
- package/dist/serialization/msg-pack-serializer.js +242 -0
- package/dist/serialization/msg-pack-serializer.js.map +1 -0
- package/dist/serialization/numpy-bridge.d.ts +96 -0
- package/dist/serialization/numpy-bridge.d.ts.map +1 -0
- package/dist/serialization/numpy-bridge.js +323 -0
- package/dist/serialization/numpy-bridge.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/addon.d.ts +131 -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 +236 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +61 -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/error-translator.d.ts +83 -0
- package/dist/utils/error-translator.d.ts.map +1 -0
- package/dist/utils/error-translator.js +210 -0
- package/dist/utils/error-translator.js.map +1 -0
- package/dist/utils/locking.d.ts +3 -0
- package/dist/utils/locking.d.ts.map +1 -0
- package/dist/utils/locking.js +10 -0
- package/dist/utils/locking.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/memory-monitor.d.ts +43 -0
- package/dist/utils/memory-monitor.d.ts.map +1 -0
- package/dist/utils/memory-monitor.js +143 -0
- package/dist/utils/memory-monitor.js.map +1 -0
- package/package.json +93 -17
- package/prebuilds/darwin-arm64/pynect-prebuild.json +8 -0
- package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
- package/prebuilds/darwin-x64/pynect-prebuild.json +8 -0
- package/prebuilds/darwin-x64/pynect_addon.node +0 -0
- package/prebuilds/linux-arm64/pynect-prebuild.json +9 -0
- package/prebuilds/linux-arm64/pynect_addon.node +0 -0
- package/prebuilds/linux-x64/pynect-prebuild.json +9 -0
- package/prebuilds/linux-x64/pynect_addon.node +0 -0
- package/prebuilds/win32-arm64/pynect-prebuild.json +8 -0
- package/prebuilds/win32-arm64/pynect_addon.node +0 -0
- package/prebuilds/win32-x64/pynect-prebuild.json +8 -0
- package/prebuilds/win32-x64/pynect_addon.node +0 -0
- package/python/ast_inspector.py +248 -0
- package/python/error_handler.py +432 -0
- package/python/isolated_worker.py +94 -0
- package/python/pynect_dispatcher.py +382 -0
- package/python/pynect_runtime.py +574 -0
- package/python/serializer.py +373 -0
- package/python/type_inspector.py +315 -0
- package/scripts/build-native.js +50 -0
- package/scripts/cli.js +21 -0
- package/scripts/ensure-native.js +85 -0
- package/scripts/postinstall.js +40 -0
- package/scripts/pyfig.js +128 -0
- package/scripts/python-provisioner.js +23 -0
- package/scripts/smoke-python-boundary.js +103 -0
- package/src/native/gil_guard.cpp +25 -0
- package/src/native/gil_guard.h +174 -0
- package/src/native/native-addon-loader.ts +218 -0
- package/src/native/pynect_addon.cpp +942 -0
- package/src/native/python_bridge.cpp +904 -0
- package/src/native/python_bridge.h +263 -0
- package/src/native/thread_pool.cpp +375 -0
- package/src/native/thread_pool.h +185 -0
- package/src/native/type_converter.cpp +906 -0
- package/src/native/type_converter.h +271 -0
- package/index.d.ts +0 -3
- package/index.js +0 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { ensurePynectPython } = require('./pyfig');
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const projectRoot = process.env.PYNECT_PROJECT_ROOT || process.env.INIT_CWD || ROOT;
|
|
12
|
+
const { python } = await ensurePynectPython({
|
|
13
|
+
projectRoot,
|
|
14
|
+
pythonPath: process.env.PYNECT_PYTHON,
|
|
15
|
+
});
|
|
16
|
+
const embedding = python.embedding;
|
|
17
|
+
const nodeGyp = require.resolve('node-gyp/bin/node-gyp.js');
|
|
18
|
+
const mode = process.argv.includes('--debug') ? '--debug' : '--release';
|
|
19
|
+
const args = [nodeGyp, 'rebuild', mode];
|
|
20
|
+
if (process.argv.includes('--verbose')) args.push('--verbose');
|
|
21
|
+
console.log(`[pynect build] Python ${python.version}: ${python.executable}`);
|
|
22
|
+
const env = {
|
|
23
|
+
...process.env,
|
|
24
|
+
npm_config_python: python.executable,
|
|
25
|
+
PYTHON: python.executable,
|
|
26
|
+
PYNECT_PYTHON_INCLUDE_DIR: embedding.includeDirectory,
|
|
27
|
+
PYNECT_PYTHON_LIBRARY: embedding.libraryPath,
|
|
28
|
+
PYNECT_PYTHON_LIBRARY_DIR: embedding.libraryDirectory,
|
|
29
|
+
PATH: [embedding.libraryDirectory, path.dirname(python.executable), process.env.PATH || '']
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(path.delimiter),
|
|
32
|
+
};
|
|
33
|
+
const result = spawnSync(process.execPath, args, { cwd: ROOT, env, stdio: 'inherit', windowsHide: true });
|
|
34
|
+
if (result.status !== 0) throw new Error(`node-gyp failed with exit code ${result.status}`);
|
|
35
|
+
const target = path.join(ROOT, 'build', mode === '--debug' ? 'Debug' : 'Release', 'pynect_addon.node');
|
|
36
|
+
if (!fs.existsSync(target)) throw new Error(`Native addon was not created at ${target}`);
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
path.join(ROOT, 'build', 'pynect-python.json'),
|
|
39
|
+
`${JSON.stringify({
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
pythonExecutable: path.resolve(python.executable),
|
|
42
|
+
pythonVersion: python.version,
|
|
43
|
+
pythonLibrary: path.resolve(embedding.libraryPath),
|
|
44
|
+
buildType: mode === '--debug' ? 'Debug' : 'Release',
|
|
45
|
+
}, null, 2)}\n`,
|
|
46
|
+
);
|
|
47
|
+
console.log(`[pynect build] Built ${path.relative(ROOT, target)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch(error => { console.error(`[pynect build] ${error.message}`); process.exitCode = 1; });
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const packageJson = require('../package.json');
|
|
6
|
+
const { createCodegenCommand } = require('../dist/cli/codegen');
|
|
7
|
+
const { createSyncCommand } = require('../dist/cli/sync');
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
.name('pynect')
|
|
11
|
+
.description('Generate types and manage Pynect projects.')
|
|
12
|
+
.version(packageJson.version)
|
|
13
|
+
.showHelpAfterError();
|
|
14
|
+
|
|
15
|
+
program.addCommand(createCodegenCommand());
|
|
16
|
+
program.addCommand(createSyncCommand());
|
|
17
|
+
|
|
18
|
+
program.parseAsync(process.argv).catch(error => {
|
|
19
|
+
console.error(`pynect: ${error.message}`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const BUILD_DIRECTORY = path.join(ROOT, 'build');
|
|
9
|
+
const METADATA = path.join(BUILD_DIRECTORY, 'pynect-python.json');
|
|
10
|
+
|
|
11
|
+
function normalize(value) {
|
|
12
|
+
const resolved = path.resolve(value);
|
|
13
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function matchingAddonPath() {
|
|
17
|
+
const selected = process.env.PYNECT_PYTHON;
|
|
18
|
+
if (!selected || !fs.existsSync(METADATA)) return null;
|
|
19
|
+
try {
|
|
20
|
+
const metadata = JSON.parse(fs.readFileSync(METADATA, 'utf8'));
|
|
21
|
+
if (
|
|
22
|
+
metadata.schemaVersion !== 1 ||
|
|
23
|
+
normalize(metadata.pythonExecutable) !== normalize(selected)
|
|
24
|
+
) return null;
|
|
25
|
+
return path.join(
|
|
26
|
+
BUILD_DIRECTORY,
|
|
27
|
+
metadata.buildType === 'Debug' ? 'Debug' : 'Release',
|
|
28
|
+
'pynect_addon.node',
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loads() {
|
|
36
|
+
const addon = matchingAddonPath();
|
|
37
|
+
if (!addon || !fs.existsSync(addon)) return false;
|
|
38
|
+
try {
|
|
39
|
+
require(addon);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function build() {
|
|
47
|
+
console.log(`[pynect native] Building with active runtime ${process.execPath}`);
|
|
48
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, 'build-native.js')], {
|
|
49
|
+
cwd: ROOT,
|
|
50
|
+
env: process.env,
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
if (result.error) throw result.error;
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
throw new Error(`Native addon build failed with exit code ${result.status}.`);
|
|
57
|
+
}
|
|
58
|
+
if (!loads()) throw new Error('Native addon did not load after it was built.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const force = process.env.PYNECT_FORCE_BUILD === '1';
|
|
63
|
+
if (!force && loads()) return;
|
|
64
|
+
if (process.env.PYNECT_SKIP_BUILD === '1') {
|
|
65
|
+
throw new Error('Native addon is unavailable and building is disabled by PYNECT_SKIP_BUILD=1.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.mkdirSync(BUILD_DIRECTORY, { recursive: true });
|
|
69
|
+
const { withLock } = await import('loqkit');
|
|
70
|
+
await withLock(
|
|
71
|
+
{ buildDirectory: BUILD_DIRECTORY },
|
|
72
|
+
async () => build(),
|
|
73
|
+
async () => force || !loads(),
|
|
74
|
+
{
|
|
75
|
+
key: `pynect-native:${normalize(BUILD_DIRECTORY)}`,
|
|
76
|
+
name: 'Pynect native build',
|
|
77
|
+
namespace: 'pynect',
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch(error => {
|
|
83
|
+
console.error(`[pynect native] ${error.message}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function disabled(value) {
|
|
7
|
+
return value !== undefined && /^(1|true|yes|on)$/i.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
if (disabled(process.env.PYNECT_SKIP_POSTINSTALL)) return;
|
|
12
|
+
|
|
13
|
+
const projectRoot = path.resolve(
|
|
14
|
+
process.env.INIT_CWD || process.env.npm_config_local_prefix || process.cwd(),
|
|
15
|
+
);
|
|
16
|
+
const environmentModule = path.join(__dirname, '..', 'dist', 'env', 'project-environment.js');
|
|
17
|
+
|
|
18
|
+
// Published packages contain dist. A source checkout may install dependencies
|
|
19
|
+
// before its first TypeScript build, so its normal verify/build flow owns setup.
|
|
20
|
+
if (!fs.existsSync(environmentModule)) return;
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
createProjectPlan,
|
|
24
|
+
requiresProjectSync,
|
|
25
|
+
syncProject,
|
|
26
|
+
} = require(environmentModule);
|
|
27
|
+
const plan = createProjectPlan({ projectRoot });
|
|
28
|
+
if (!requiresProjectSync(plan)) return;
|
|
29
|
+
|
|
30
|
+
const result = await syncProject({ projectRoot });
|
|
31
|
+
console.log(
|
|
32
|
+
`[pynect] Synchronized ${result.plan.requirements.length} Python requirement(s) ` +
|
|
33
|
+
`with uv in ${path.dirname(result.plan.stateFile)}.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch(error => {
|
|
38
|
+
console.error(`[pynect] Postinstall synchronization failed: ${error.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
});
|
package/scripts/pyfig.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const REQUIRED_PYFIG_FUNCTIONS = [
|
|
7
|
+
'createPyfig',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function loadPyfig() {
|
|
11
|
+
const configured = process.env.PYNECT_PYFIG_PATH;
|
|
12
|
+
if (configured) {
|
|
13
|
+
return requireCompatiblePyfig(path.resolve(configured), 'PYNECT_PYFIG_PATH');
|
|
14
|
+
}
|
|
15
|
+
let packageError;
|
|
16
|
+
try {
|
|
17
|
+
return requireCompatiblePyfig('pyfig', 'installed pyfig package');
|
|
18
|
+
} catch (error) {
|
|
19
|
+
packageError = error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// During coordinated source development, a published but older package can
|
|
23
|
+
// resolve successfully. Prefer the sibling build when its API is compatible.
|
|
24
|
+
const sibling = path.resolve(__dirname, '..', '..', 'pyfig', 'dist', 'cjs', 'index.js');
|
|
25
|
+
if (fs.existsSync(sibling)) {
|
|
26
|
+
return requireCompatiblePyfig(sibling, 'sibling pyfig build');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const detail = packageError instanceof Error ? ` ${packageError.message}` : '';
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Pynect requires a Pyfig release with the venv and spawn API.${detail}`,
|
|
32
|
+
{ cause: packageError },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireCompatiblePyfig(specifier, label) {
|
|
37
|
+
const candidate = require(specifier);
|
|
38
|
+
const missing = REQUIRED_PYFIG_FUNCTIONS.filter(name => typeof candidate[name] !== 'function');
|
|
39
|
+
if (missing.length > 0) {
|
|
40
|
+
throw new Error(`${label} is missing required exports: ${missing.join(', ')}.`);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseBoolean(value, fallback) {
|
|
46
|
+
if (value === undefined) return fallback;
|
|
47
|
+
if (/^(1|true|yes|on)$/i.test(value)) return true;
|
|
48
|
+
if (/^(0|false|no|off)$/i.test(value)) return false;
|
|
49
|
+
throw new TypeError(`Invalid boolean value: ${value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function projectRoot(explicit) {
|
|
53
|
+
const hint = explicit || process.env.PYNECT_PROJECT_ROOT || process.env.INIT_CWD;
|
|
54
|
+
return hint ? path.resolve(hint) : loadPyfig().resolveProjectRoot(process.cwd());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pynectPyfigOptions(options = {}) {
|
|
58
|
+
const pyfig = loadPyfig();
|
|
59
|
+
const installPython = options.autoInstallPython ?? parseBoolean(
|
|
60
|
+
process.env.PYNECT_INSTALL_PYTHON ?? process.env.PYNECT_AUTO_INSTALL,
|
|
61
|
+
true,
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
projectRoot: projectRoot(options.projectRoot),
|
|
65
|
+
python: options.pythonPath || process.env.PYNECT_PYTHON || '',
|
|
66
|
+
pythonRange: options.pythonRange || process.env.PYNECT_REQUIRES_PYTHON ||
|
|
67
|
+
pyfig.DEFAULT_PYTHON_RANGE,
|
|
68
|
+
managedPython: options.managedPython || process.env.PYNECT_MANAGED_PYTHON ||
|
|
69
|
+
process.env.PYNECT_PYTHON_VERSION || pyfig.DEFAULT_MANAGED_PYTHON,
|
|
70
|
+
uvVersion: options.uvVersion || process.env.PYNECT_UV_VERSION || pyfig.DEFAULT_UV_VERSION,
|
|
71
|
+
requireEmbedding: true,
|
|
72
|
+
requireGil: true,
|
|
73
|
+
installPython,
|
|
74
|
+
installUv: options.autoInstallUv ?? parseBoolean(process.env.PYNECT_INSTALL_UV, true),
|
|
75
|
+
createVirtualEnvironment: options.autoCreateVirtualEnvironment ??
|
|
76
|
+
parseBoolean(process.env.PYNECT_CREATE_VIRTUAL_ENVIRONMENT, true),
|
|
77
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertEmbeddable(python) {
|
|
82
|
+
if (!python.embedding) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Python ${python.version} at ${python.executable} is missing Python.h or its CPython ` +
|
|
85
|
+
'embedding library.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return python;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createPynectPyfig(options = {}) {
|
|
92
|
+
const client = loadPyfig().createPyfig(pynectPyfigOptions(options));
|
|
93
|
+
return {
|
|
94
|
+
...client,
|
|
95
|
+
python(args = [], spawnOptions = {}) {
|
|
96
|
+
return client.uv(['run', '--', 'python', ...args], spawnOptions);
|
|
97
|
+
},
|
|
98
|
+
pythonSync(args = [], spawnOptions = {}) {
|
|
99
|
+
return client.uvSync(['run', '--', 'python', ...args], spawnOptions);
|
|
100
|
+
},
|
|
101
|
+
pip(args = [], spawnOptions = {}) {
|
|
102
|
+
return client.uv(['pip', ...args], spawnOptions);
|
|
103
|
+
},
|
|
104
|
+
pipSync(args = [], spawnOptions = {}) {
|
|
105
|
+
return client.uvSync(['pip', ...args], spawnOptions);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function ensurePynectPython(options = {}) {
|
|
111
|
+
const client = createPynectPyfig(options);
|
|
112
|
+
const python = assertEmbeddable(await client.pythonInfo());
|
|
113
|
+
return { client, python };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function ensurePynectEnvironment(options = {}) {
|
|
117
|
+
const client = createPynectPyfig(options);
|
|
118
|
+
const environment = await client.prepare();
|
|
119
|
+
assertEmbeddable(environment.python);
|
|
120
|
+
return { client, environment };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
createPynectPyfig,
|
|
125
|
+
ensurePynectEnvironment,
|
|
126
|
+
ensurePynectPython,
|
|
127
|
+
projectRoot,
|
|
128
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ensurePynectPython } = require('./pyfig');
|
|
4
|
+
|
|
5
|
+
async function ensurePython(options = {}) {
|
|
6
|
+
const { python } = await ensurePynectPython({
|
|
7
|
+
autoInstallPython: options.autoInstall,
|
|
8
|
+
managedPython: options.version,
|
|
9
|
+
pythonPath: options.pythonPath,
|
|
10
|
+
projectRoot: options.projectRoot,
|
|
11
|
+
});
|
|
12
|
+
return python;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function detectPython(explicit) {
|
|
16
|
+
try {
|
|
17
|
+
return await ensurePython({ autoInstall: false, pythonPath: explicit });
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { detectPython, ensurePython };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { ensurePynectEnvironment } = require('./pyfig');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const { client, environment } = await ensurePynectEnvironment({ projectRoot: ROOT });
|
|
10
|
+
const { python } = environment;
|
|
11
|
+
const expected = normalize(python.executable);
|
|
12
|
+
const pynect = require('../dist');
|
|
13
|
+
|
|
14
|
+
const detector = new pynect.PythonDetector({ projectRoot: ROOT });
|
|
15
|
+
const detected = await detector.detect();
|
|
16
|
+
assertSamePython(detected.executable, expected, 'PythonDetector');
|
|
17
|
+
const probed = detector.probe(detected.executable);
|
|
18
|
+
if (!probed) throw new Error('PythonDetector could not probe the selected repository Python.');
|
|
19
|
+
assertSamePython(probed.executable, expected, 'PythonDetector.probe');
|
|
20
|
+
|
|
21
|
+
const selected = client.pythonSync(
|
|
22
|
+
['-c', 'import sys; print(sys.executable)'],
|
|
23
|
+
{ encoding: 'utf8' },
|
|
24
|
+
);
|
|
25
|
+
assertSuccess(selected, 'PyfigClient.pythonSync');
|
|
26
|
+
assertSamePython(selected.stdout.trim(), expected, 'PyfigClient.pythonSync');
|
|
27
|
+
|
|
28
|
+
const pip = client.pipSync(['list', '--format', 'json'], { encoding: 'utf8' });
|
|
29
|
+
assertSuccess(pip, 'PyfigClient.pipSync');
|
|
30
|
+
|
|
31
|
+
const uvPip = client.uvSync(
|
|
32
|
+
['pip', 'list', '--format', 'json'],
|
|
33
|
+
{ encoding: 'utf8' },
|
|
34
|
+
);
|
|
35
|
+
assertSuccess(uvPip, 'PyfigClient.uvSync pip');
|
|
36
|
+
|
|
37
|
+
const inspector = client.pythonSync(
|
|
38
|
+
[path.join(ROOT, 'python', 'type_inspector.py'), 'json'],
|
|
39
|
+
{ encoding: 'utf8' },
|
|
40
|
+
);
|
|
41
|
+
assertSuccess(inspector, 'codegen inspector');
|
|
42
|
+
JSON.parse(inspector.stdout);
|
|
43
|
+
|
|
44
|
+
const worker = client.pythonSync(
|
|
45
|
+
[path.join(ROOT, 'python', 'isolated_worker.py')],
|
|
46
|
+
{
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
input: `${JSON.stringify({
|
|
49
|
+
id: 1,
|
|
50
|
+
module: 'os',
|
|
51
|
+
function: 'getenv',
|
|
52
|
+
args: ['VIRTUAL_ENV'],
|
|
53
|
+
kwargs: {},
|
|
54
|
+
})}\n`,
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
PYTHONPATH: path.join(ROOT, 'python'),
|
|
58
|
+
PYNECT_DISPATCHER_CONFIG: '{}',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
assertSuccess(worker, 'isolated worker');
|
|
63
|
+
const workerResult = JSON.parse(worker.stdout.trim());
|
|
64
|
+
if (!workerResult.ok || normalize(workerResult.result) !== normalize(python.virtualEnvironment)) {
|
|
65
|
+
throw new Error('The isolated worker did not use the Pyfig virtual environment.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await pynect.init({
|
|
69
|
+
projectRoot: ROOT,
|
|
70
|
+
allowUnsafeCode: true,
|
|
71
|
+
logLevel: 'error',
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const embedded = await pynect.py.eval('__import__("sys").executable');
|
|
75
|
+
assertSamePython(embedded, expected, 'embedded CPython');
|
|
76
|
+
} finally {
|
|
77
|
+
await pynect.shutdown();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Python boundary verified: ${python.executable}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertSuccess(result, label) {
|
|
84
|
+
if (result.status !== 0 || result.error) {
|
|
85
|
+
throw new Error(`${label} failed: ${String(result.stderr || result.error?.message || result.status)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertSamePython(actual, expected, label) {
|
|
90
|
+
if (normalize(actual) !== expected) {
|
|
91
|
+
throw new Error(`${label} used ${actual}; expected ${expected}.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalize(value) {
|
|
96
|
+
const resolved = path.resolve(String(value || ''));
|
|
97
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch(error => {
|
|
101
|
+
console.error(`[pynect smoke] ${error.message}`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pynect - GIL Guard Implementation
|
|
3
|
+
* The GIL guard is header-only; this file provides explicit template
|
|
4
|
+
* instantiations and any platform-specific workarounds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#include "gil_guard.h"
|
|
8
|
+
|
|
9
|
+
// All implementation is in the header file (RAII classes are trivial inline).
|
|
10
|
+
// This .cpp exists to satisfy the build system and future non-inline additions.
|
|
11
|
+
|
|
12
|
+
namespace pynect {
|
|
13
|
+
|
|
14
|
+
// Verify at compile time that our RAII types are not accidentally copied
|
|
15
|
+
static_assert(!std::is_copy_constructible<GILAcquire>::value,
|
|
16
|
+
"GILAcquire must not be copy-constructible");
|
|
17
|
+
static_assert(!std::is_copy_constructible<GILRelease>::value,
|
|
18
|
+
"GILRelease must not be copy-constructible");
|
|
19
|
+
static_assert(!std::is_copy_constructible<PyRef>::value,
|
|
20
|
+
"PyRef must not be copy-constructible");
|
|
21
|
+
|
|
22
|
+
static_assert(std::is_move_constructible<PyRef>::value,
|
|
23
|
+
"PyRef must be move-constructible");
|
|
24
|
+
|
|
25
|
+
} // namespace pynect
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pynect - GIL Guard Header
|
|
3
|
+
* RAII wrappers for the Python Global Interpreter Lock (GIL).
|
|
4
|
+
*
|
|
5
|
+
* The GIL ensures only one thread executes Python bytecode at a time.
|
|
6
|
+
* These guards make GIL management safe via RAII idiom.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#pragma once
|
|
10
|
+
|
|
11
|
+
#include <Python.h>
|
|
12
|
+
#include <stdexcept>
|
|
13
|
+
|
|
14
|
+
namespace pynect {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GILAcquire - RAII guard that acquires the GIL on construction
|
|
18
|
+
* and releases it on destruction.
|
|
19
|
+
*
|
|
20
|
+
* Use when Python code needs to run in a non-Python thread (e.g. a worker thread).
|
|
21
|
+
* The calling thread must NOT already hold the GIL.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```cpp
|
|
25
|
+
* void worker_thread_func() {
|
|
26
|
+
* GILAcquire gil; // GIL acquired
|
|
27
|
+
* PyObject* result = PyEval_EvalCode(...);
|
|
28
|
+
* // GIL released when gil goes out of scope
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
class GILAcquire {
|
|
33
|
+
public:
|
|
34
|
+
GILAcquire() noexcept : _state(PyGILState_Ensure()) {}
|
|
35
|
+
~GILAcquire() noexcept { PyGILState_Release(_state); }
|
|
36
|
+
|
|
37
|
+
// Non-copyable, non-movable
|
|
38
|
+
GILAcquire(const GILAcquire&) = delete;
|
|
39
|
+
GILAcquire& operator=(const GILAcquire&) = delete;
|
|
40
|
+
GILAcquire(GILAcquire&&) = delete;
|
|
41
|
+
GILAcquire& operator=(GILAcquire&&) = delete;
|
|
42
|
+
|
|
43
|
+
private:
|
|
44
|
+
PyGILState_STATE _state;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* GILRelease - RAII guard that releases the GIL on construction
|
|
49
|
+
* and re-acquires it on destruction.
|
|
50
|
+
*
|
|
51
|
+
* Use to temporarily release the GIL while performing blocking I/O or
|
|
52
|
+
* other operations that don't need Python, allowing other Python threads to run.
|
|
53
|
+
* The calling thread MUST already hold the GIL.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```cpp
|
|
57
|
+
* // Inside Python-holding code:
|
|
58
|
+
* {
|
|
59
|
+
* GILRelease release; // GIL released - other Python threads can run
|
|
60
|
+
* perform_blocking_io();
|
|
61
|
+
* } // GIL re-acquired here
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
class GILRelease {
|
|
65
|
+
public:
|
|
66
|
+
GILRelease() noexcept : _state(PyEval_SaveThread()) {}
|
|
67
|
+
~GILRelease() noexcept { PyEval_RestoreThread(_state); }
|
|
68
|
+
|
|
69
|
+
// Non-copyable, non-movable
|
|
70
|
+
GILRelease(const GILRelease&) = delete;
|
|
71
|
+
GILRelease& operator=(const GILRelease&) = delete;
|
|
72
|
+
GILRelease(GILRelease&&) = delete;
|
|
73
|
+
GILRelease& operator=(GILRelease&&) = delete;
|
|
74
|
+
|
|
75
|
+
private:
|
|
76
|
+
PyThreadState* _state;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* PyRef - RAII wrapper for PyObject* reference counting.
|
|
81
|
+
*
|
|
82
|
+
* Automatically calls Py_XDECREF on destruction.
|
|
83
|
+
* Use steal() for objects returned from Python C API with new references.
|
|
84
|
+
* Use borrow() for borrowed references (won't decref).
|
|
85
|
+
*/
|
|
86
|
+
class PyRef {
|
|
87
|
+
public:
|
|
88
|
+
PyRef() noexcept : _obj(nullptr), _owned(false) {}
|
|
89
|
+
|
|
90
|
+
~PyRef() noexcept {
|
|
91
|
+
reset();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Take ownership of an object (new reference from Python API) */
|
|
95
|
+
static PyRef steal(PyObject* obj) noexcept {
|
|
96
|
+
PyRef ref;
|
|
97
|
+
ref._obj = obj;
|
|
98
|
+
ref._owned = true;
|
|
99
|
+
return ref;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Borrow a reference (will Py_INCREF and Py_DECREF) */
|
|
103
|
+
static PyRef borrow(PyObject* obj) noexcept {
|
|
104
|
+
PyRef ref;
|
|
105
|
+
ref._obj = obj;
|
|
106
|
+
ref._owned = false;
|
|
107
|
+
if (obj) {
|
|
108
|
+
Py_INCREF(obj);
|
|
109
|
+
ref._owned = true;
|
|
110
|
+
}
|
|
111
|
+
return ref;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Increment reference count and get raw pointer */
|
|
115
|
+
PyObject* incref() const noexcept {
|
|
116
|
+
Py_XINCREF(_obj);
|
|
117
|
+
return _obj;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get raw pointer without changing reference count */
|
|
121
|
+
PyObject* get() const noexcept { return _obj; }
|
|
122
|
+
|
|
123
|
+
/** Release ownership without decrementing ref count */
|
|
124
|
+
PyObject* release() noexcept {
|
|
125
|
+
PyObject* obj = _obj;
|
|
126
|
+
_obj = nullptr;
|
|
127
|
+
_owned = false;
|
|
128
|
+
return obj;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Reset - decrements ref count if owned */
|
|
132
|
+
void reset() noexcept {
|
|
133
|
+
if (_owned && _obj) {
|
|
134
|
+
Py_DECREF(_obj);
|
|
135
|
+
}
|
|
136
|
+
_obj = nullptr;
|
|
137
|
+
_owned = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Check if the object is valid (non-null) */
|
|
141
|
+
explicit operator bool() const noexcept { return _obj != nullptr; }
|
|
142
|
+
|
|
143
|
+
/** Check for Python exception state */
|
|
144
|
+
bool hasException() const noexcept {
|
|
145
|
+
return _obj == nullptr && PyErr_Occurred() != nullptr;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Non-copyable
|
|
149
|
+
PyRef(const PyRef&) = delete;
|
|
150
|
+
PyRef& operator=(const PyRef&) = delete;
|
|
151
|
+
|
|
152
|
+
// Movable
|
|
153
|
+
PyRef(PyRef&& other) noexcept : _obj(other._obj), _owned(other._owned) {
|
|
154
|
+
other._obj = nullptr;
|
|
155
|
+
other._owned = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
PyRef& operator=(PyRef&& other) noexcept {
|
|
159
|
+
if (this != &other) {
|
|
160
|
+
reset();
|
|
161
|
+
_obj = other._obj;
|
|
162
|
+
_owned = other._owned;
|
|
163
|
+
other._obj = nullptr;
|
|
164
|
+
other._owned = false;
|
|
165
|
+
}
|
|
166
|
+
return *this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private:
|
|
170
|
+
PyObject* _obj;
|
|
171
|
+
bool _owned;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
} // namespace pynect
|