pynect 0.2.6 → 0.2.9
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/README.md +50 -4
- package/binding.gyp +2 -1
- package/dist/cli/codegen.d.ts +3 -0
- package/dist/cli/codegen.d.ts.map +1 -1
- package/dist/cli/codegen.js +12 -0
- package/dist/cli/codegen.js.map +1 -1
- package/dist/config/project.d.ts +7 -0
- package/dist/config/project.d.ts.map +1 -1
- package/dist/config/project.js +23 -2
- package/dist/config/project.js.map +1 -1
- package/dist/core/py-proxy.d.ts +1 -1
- package/dist/core/py-proxy.d.ts.map +1 -1
- package/dist/core/py-proxy.js +40 -7
- package/dist/core/py-proxy.js.map +1 -1
- package/dist/core/py-runtime.d.ts +7 -0
- package/dist/core/py-runtime.d.ts.map +1 -1
- package/dist/core/py-runtime.js +50 -3
- package/dist/core/py-runtime.js.map +1 -1
- package/dist/core/pynect-callback.d.ts +25 -0
- package/dist/core/pynect-callback.d.ts.map +1 -0
- package/dist/core/pynect-callback.js +99 -0
- package/dist/core/pynect-callback.js.map +1 -0
- package/dist/env/project-environment.d.ts.map +1 -1
- package/dist/env/project-environment.js +1 -0
- package/dist/env/project-environment.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/native/native-addon-loader.d.ts.map +1 -1
- package/dist/native/native-addon-loader.js +34 -10
- package/dist/native/native-addon-loader.js.map +1 -1
- package/dist/types/addon.d.ts +10 -0
- package/dist/types/addon.d.ts.map +1 -1
- package/dist/types/addon.js.map +1 -1
- package/dist/types/config.d.ts +10 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/python.d.ts +2 -0
- package/dist/types/python.d.ts.map +1 -1
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/pynect-prebuild.json +5 -4
- package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
- package/prebuilds/darwin-x64/pynect-prebuild.json +5 -4
- package/prebuilds/darwin-x64/pynect_addon.node +0 -0
- package/prebuilds/linux-arm64/pynect-prebuild.json +5 -4
- package/prebuilds/linux-arm64/pynect_addon.node +0 -0
- package/prebuilds/linux-x64/pynect-prebuild.json +5 -4
- package/prebuilds/linux-x64/pynect_addon.node +0 -0
- package/prebuilds/win32-arm64/pynect-prebuild.json +5 -4
- package/prebuilds/win32-arm64/pynect_addon.node +0 -0
- package/prebuilds/win32-x64/pynect-prebuild.json +5 -4
- package/prebuilds/win32-x64/pynect_addon.node +0 -0
- package/python/type_inspector.py +12 -0
- package/scripts/build-native.js +9 -3
- package/scripts/ensure-native.js +8 -2
- package/scripts/smoke-python-boundary.js +71 -1
- package/src/native/callback_bridge.h +11 -0
- package/src/native/native-addon-loader.ts +39 -11
- package/src/native/pynect_addon.cpp +354 -4
- package/src/native/python_bridge.cpp +117 -40
- package/src/native/python_bridge.h +13 -0
- package/src/native/thread_pool.cpp +62 -55
- package/src/native/thread_pool.h +13 -0
- package/src/native/type_converter.cpp +32 -22
- package/src/native/type_converter.h +2 -0
package/scripts/build-native.js
CHANGED
|
@@ -14,6 +14,12 @@ async function main() {
|
|
|
14
14
|
pythonPath: process.env.PYNECT_PYTHON,
|
|
15
15
|
});
|
|
16
16
|
const embedding = python.embedding;
|
|
17
|
+
const stableAbiLibrary = process.platform === 'win32'
|
|
18
|
+
? embedding.stableAbiLibraryPath || path.join(embedding.libraryDirectory, 'python3.lib')
|
|
19
|
+
: '';
|
|
20
|
+
const linkLibrary = stableAbiLibrary && fs.existsSync(stableAbiLibrary)
|
|
21
|
+
? stableAbiLibrary
|
|
22
|
+
: embedding.libraryPath;
|
|
17
23
|
const nodeGyp = require.resolve('node-gyp/bin/node-gyp.js');
|
|
18
24
|
const mode = process.argv.includes('--debug') ? '--debug' : '--release';
|
|
19
25
|
const args = [nodeGyp, 'rebuild', mode];
|
|
@@ -24,8 +30,8 @@ async function main() {
|
|
|
24
30
|
npm_config_python: python.executable,
|
|
25
31
|
PYTHON: python.executable,
|
|
26
32
|
PYNECT_PYTHON_INCLUDE_DIR: embedding.includeDirectory,
|
|
27
|
-
PYNECT_PYTHON_LIBRARY:
|
|
28
|
-
PYNECT_PYTHON_LIBRARY_DIR:
|
|
33
|
+
PYNECT_PYTHON_LIBRARY: linkLibrary,
|
|
34
|
+
PYNECT_PYTHON_LIBRARY_DIR: path.dirname(linkLibrary),
|
|
29
35
|
PATH: [embedding.libraryDirectory, path.dirname(python.executable), process.env.PATH || '']
|
|
30
36
|
.filter(Boolean)
|
|
31
37
|
.join(path.delimiter),
|
|
@@ -40,7 +46,7 @@ async function main() {
|
|
|
40
46
|
schemaVersion: 1,
|
|
41
47
|
pythonExecutable: path.resolve(python.executable),
|
|
42
48
|
pythonVersion: python.version,
|
|
43
|
-
pythonLibrary: path.resolve(
|
|
49
|
+
pythonLibrary: path.resolve(linkLibrary),
|
|
44
50
|
buildType: mode === '--debug' ? 'Debug' : 'Release',
|
|
45
51
|
}, null, 2)}\n`,
|
|
46
52
|
);
|
package/scripts/ensure-native.js
CHANGED
|
@@ -7,6 +7,7 @@ const { spawnSync } = require('child_process');
|
|
|
7
7
|
const ROOT = path.resolve(__dirname, '..');
|
|
8
8
|
const BUILD_DIRECTORY = path.join(ROOT, 'build');
|
|
9
9
|
const METADATA = path.join(BUILD_DIRECTORY, 'pynect-python.json');
|
|
10
|
+
let lastLoadError;
|
|
10
11
|
|
|
11
12
|
function normalize(value) {
|
|
12
13
|
const resolved = path.resolve(value);
|
|
@@ -33,12 +34,14 @@ function matchingAddonPath() {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function loads() {
|
|
37
|
+
lastLoadError = undefined;
|
|
36
38
|
const addon = matchingAddonPath();
|
|
37
39
|
if (!addon || !fs.existsSync(addon)) return false;
|
|
38
40
|
try {
|
|
39
41
|
require(addon);
|
|
40
42
|
return true;
|
|
41
|
-
} catch {
|
|
43
|
+
} catch (error) {
|
|
44
|
+
lastLoadError = error;
|
|
42
45
|
return false;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -55,7 +58,10 @@ function build() {
|
|
|
55
58
|
if (result.status !== 0) {
|
|
56
59
|
throw new Error(`Native addon build failed with exit code ${result.status}.`);
|
|
57
60
|
}
|
|
58
|
-
if (!loads())
|
|
61
|
+
if (!loads()) {
|
|
62
|
+
const detail = lastLoadError instanceof Error ? ` ${lastLoadError.message}` : '';
|
|
63
|
+
throw new Error(`Native addon did not load after it was built.${detail}`);
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
async function main() {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
process.env.PYNECT_PREFER_LOCAL ??= '1';
|
|
4
|
+
|
|
3
5
|
const path = require('path');
|
|
4
6
|
const { ensurePynectEnvironment } = require('./pyfig');
|
|
5
7
|
|
|
@@ -40,6 +42,22 @@ async function main() {
|
|
|
40
42
|
);
|
|
41
43
|
assertSuccess(inspector, 'codegen inspector');
|
|
42
44
|
JSON.parse(inspector.stdout);
|
|
45
|
+
const callbackInspection = client.pythonSync(
|
|
46
|
+
[path.join(ROOT, 'python', 'type_inspector.py'), 'pynect_callback_fixture'],
|
|
47
|
+
{
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
PYTHONPATH: path.join(ROOT, 'tests', 'fixtures'),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
assertSuccess(callbackInspection, 'callback type inspector');
|
|
56
|
+
const inspectedCallback = JSON.parse(callbackInspection.stdout).functions
|
|
57
|
+
.find(item => item.name === 'invoke')?.parameters?.[0]?.type;
|
|
58
|
+
if (inspectedCallback !== 'Callable[[Any], Any]') {
|
|
59
|
+
throw new Error(`Callback annotation was inspected as ${String(inspectedCallback)}.`);
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
const worker = client.pythonSync(
|
|
45
63
|
[path.join(ROOT, 'python', 'isolated_worker.py')],
|
|
@@ -69,15 +87,67 @@ async function main() {
|
|
|
69
87
|
projectRoot: ROOT,
|
|
70
88
|
allowUnsafeCode: true,
|
|
71
89
|
logLevel: 'error',
|
|
90
|
+
pythonPath_extra: [path.join(ROOT, 'tests', 'fixtures')],
|
|
91
|
+
threadPool: { size: 1 },
|
|
92
|
+
callbacks: { maxDepth: 2 },
|
|
72
93
|
});
|
|
73
94
|
try {
|
|
74
95
|
const embedded = await pynect.py.eval('__import__("sys").executable');
|
|
75
96
|
assertSamePython(embedded, expected, 'embedded CPython');
|
|
97
|
+
const fixture = await pynect.importModule('pynect_callback_fixture');
|
|
98
|
+
await assertRejects(
|
|
99
|
+
() => fixture.invoke((value) => value, 1),
|
|
100
|
+
/ctx\.callback\(\)/,
|
|
101
|
+
'unregistered callback',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
let disposedCallback;
|
|
105
|
+
await pynect.withCallbacks(async ctx => {
|
|
106
|
+
const asyncCallback = ctx.callback(async value => {
|
|
107
|
+
await Promise.resolve();
|
|
108
|
+
return value * 3;
|
|
109
|
+
});
|
|
110
|
+
if (await fixture.invoke(asyncCallback, 4) !== 12) {
|
|
111
|
+
throw new Error('Async JavaScript callback returned the wrong value.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const reentrantCallback = ctx.callback(async value => fixture.double(value));
|
|
115
|
+
if (await fixture.invoke(reentrantCallback, 5) !== 10) {
|
|
116
|
+
throw new Error('Reentrant JavaScript callback returned the wrong value.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let recursiveCallback;
|
|
120
|
+
recursiveCallback = ctx.callback(async depth => {
|
|
121
|
+
if (depth <= 0) return 0;
|
|
122
|
+
return fixture.invoke(recursiveCallback, depth - 1);
|
|
123
|
+
});
|
|
124
|
+
await assertRejects(
|
|
125
|
+
() => fixture.invoke(recursiveCallback, 2),
|
|
126
|
+
/recursion depth 3 exceeds the configured maximum of 2/,
|
|
127
|
+
'callback recursion limit',
|
|
128
|
+
);
|
|
129
|
+
disposedCallback = asyncCallback;
|
|
130
|
+
});
|
|
131
|
+
await assertRejects(
|
|
132
|
+
() => fixture.invoke(disposedCallback, 1),
|
|
133
|
+
/disposed/,
|
|
134
|
+
'callback context disposal',
|
|
135
|
+
);
|
|
76
136
|
} finally {
|
|
77
137
|
await pynect.shutdown();
|
|
78
138
|
}
|
|
79
139
|
|
|
80
|
-
console.log(`Python boundary verified: ${python.executable}`);
|
|
140
|
+
console.log(`Python ${python.version} boundary verified: ${python.executable}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function assertRejects(action, pattern, label) {
|
|
144
|
+
try {
|
|
145
|
+
await action();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (pattern.test(String(error?.message || error))) return;
|
|
148
|
+
throw new Error(`${label} failed with an unexpected error: ${error?.message || error}`);
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`${label} did not reject.`);
|
|
81
151
|
}
|
|
82
152
|
|
|
83
153
|
function assertSuccess(result, label) {
|
|
@@ -18,15 +18,24 @@ export interface NativeAddonBuildOptions {
|
|
|
18
18
|
export function loadOrBuildNativeAddon(options: NativeAddonBuildOptions): NativeAddon {
|
|
19
19
|
let initialError: unknown = new Error('Native rebuild explicitly requested.');
|
|
20
20
|
if (process.env['PYNECT_FORCE_BUILD'] !== '1') {
|
|
21
|
+
if (process.env['PYNECT_PREFER_LOCAL'] === '1') {
|
|
22
|
+
try {
|
|
23
|
+
return loadMatchingLocalAddon(options);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
initialError = error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
21
28
|
try {
|
|
22
29
|
return loadMatchingPrebuild(options);
|
|
23
30
|
} catch (error) {
|
|
24
31
|
initialError = error;
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
if (process.env['PYNECT_PREFER_LOCAL'] !== '1') {
|
|
34
|
+
try {
|
|
35
|
+
return loadMatchingLocalAddon(options);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
initialError = new AggregateError([initialError, error], 'No compatible native addon loaded.');
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -72,10 +81,11 @@ interface NativeBuildMetadata {
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
interface NativePrebuildMetadata {
|
|
75
|
-
schemaVersion:
|
|
84
|
+
schemaVersion: 2;
|
|
76
85
|
platform: NodeJS.Platform;
|
|
77
86
|
architecture: string;
|
|
78
|
-
|
|
87
|
+
pythonMinimumVersion: string;
|
|
88
|
+
pythonAbi: string;
|
|
79
89
|
napi: number;
|
|
80
90
|
glibc?: string;
|
|
81
91
|
pythonLibraryName: string;
|
|
@@ -91,17 +101,22 @@ function loadMatchingPrebuild(options: NativeAddonBuildOptions): NativeAddon {
|
|
|
91
101
|
} catch (error) {
|
|
92
102
|
throw new Error(`Native prebuild metadata is unavailable at ${metadataPath}.`, { cause: error });
|
|
93
103
|
}
|
|
94
|
-
const
|
|
104
|
+
const pythonVersion = majorMinor(options.pythonVersion);
|
|
105
|
+
const minimumVersion = typeof metadata.pythonMinimumVersion === 'string'
|
|
106
|
+
? majorMinor(metadata.pythonMinimumVersion)
|
|
107
|
+
: [Number.POSITIVE_INFINITY, 0] as const;
|
|
95
108
|
if (
|
|
96
|
-
metadata.schemaVersion !==
|
|
109
|
+
metadata.schemaVersion !== 2 ||
|
|
97
110
|
metadata.platform !== process.platform ||
|
|
98
111
|
metadata.architecture !== process.arch ||
|
|
99
|
-
metadata.
|
|
112
|
+
metadata.pythonAbi !== 'cp310-abi3' ||
|
|
113
|
+
pythonVersion[0] !== 3 ||
|
|
114
|
+
compareMajorMinor(pythonVersion, minimumVersion) < 0 ||
|
|
100
115
|
metadata.napi > Number(process.versions.napi ?? 0)
|
|
101
116
|
) {
|
|
102
117
|
throw new Error(
|
|
103
118
|
`Native prebuild does not match ${process.platform}-${process.arch}, ` +
|
|
104
|
-
`Python ${
|
|
119
|
+
`Python ${options.pythonVersion}, N-API ${process.versions.napi ?? 'unknown'}.`,
|
|
105
120
|
);
|
|
106
121
|
}
|
|
107
122
|
if (process.platform === 'linux' && metadata.glibc) {
|
|
@@ -137,7 +152,7 @@ function materializePortablePrebuild(
|
|
|
137
152
|
'node_modules',
|
|
138
153
|
'.pynect',
|
|
139
154
|
'native',
|
|
140
|
-
`${process.platform}-${process.arch}
|
|
155
|
+
`${process.platform}-${process.arch}-${metadata.pythonAbi}-${identity}`,
|
|
141
156
|
);
|
|
142
157
|
const addon = path.join(directory, 'pynect_addon.node');
|
|
143
158
|
const library = path.join(directory, metadata.pythonLibraryName);
|
|
@@ -216,3 +231,16 @@ function compareVersions(left: string, right: string): number {
|
|
|
216
231
|
}
|
|
217
232
|
return 0;
|
|
218
233
|
}
|
|
234
|
+
|
|
235
|
+
function majorMinor(version: string): readonly [number, number] {
|
|
236
|
+
const match = version.match(/^(\d+)\.(\d+)/u);
|
|
237
|
+
if (!match) throw new Error(`Invalid Python version: ${version}`);
|
|
238
|
+
return [Number(match[1]), Number(match[2])];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function compareMajorMinor(
|
|
242
|
+
left: readonly [number, number],
|
|
243
|
+
right: readonly [number, number],
|
|
244
|
+
): number {
|
|
245
|
+
return left[0] - right[0] || left[1] - right[1];
|
|
246
|
+
}
|