pynect 0.2.7 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +68 -27
  2. package/SECURITY.md +6 -6
  3. package/dist/cli/codegen.d.ts +3 -0
  4. package/dist/cli/codegen.d.ts.map +1 -1
  5. package/dist/cli/codegen.js +15 -3
  6. package/dist/cli/codegen.js.map +1 -1
  7. package/dist/cli/sync.d.ts.map +1 -1
  8. package/dist/cli/sync.js +38 -19
  9. package/dist/cli/sync.js.map +1 -1
  10. package/dist/config/project.d.ts +8 -52
  11. package/dist/config/project.d.ts.map +1 -1
  12. package/dist/config/project.js +74 -121
  13. package/dist/config/project.js.map +1 -1
  14. package/dist/core/isolated-worker.js +4 -4
  15. package/dist/core/isolated-worker.js.map +1 -1
  16. package/dist/core/py-proxy.d.ts +2 -1
  17. package/dist/core/py-proxy.d.ts.map +1 -1
  18. package/dist/core/py-proxy.js +55 -7
  19. package/dist/core/py-proxy.js.map +1 -1
  20. package/dist/core/py-runtime.d.ts +1 -5
  21. package/dist/core/py-runtime.d.ts.map +1 -1
  22. package/dist/core/py-runtime.js +36 -100
  23. package/dist/core/py-runtime.js.map +1 -1
  24. package/dist/core/pynect-callback.d.ts +16 -0
  25. package/dist/core/pynect-callback.d.ts.map +1 -0
  26. package/dist/core/pynect-callback.js +52 -0
  27. package/dist/core/pynect-callback.js.map +1 -0
  28. package/dist/env/project-environment.d.ts +9 -30
  29. package/dist/env/project-environment.d.ts.map +1 -1
  30. package/dist/env/project-environment.js +32 -217
  31. package/dist/env/project-environment.js.map +1 -1
  32. package/dist/env/python.d.ts +16 -0
  33. package/dist/env/python.d.ts.map +1 -0
  34. package/dist/env/python.js +9 -0
  35. package/dist/env/python.js.map +1 -0
  36. package/dist/index.d.ts +8 -24
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -31
  39. package/dist/index.js.map +1 -1
  40. package/dist/native/native-addon-loader.d.ts.map +1 -1
  41. package/dist/native/native-addon-loader.js +15 -5
  42. package/dist/native/native-addon-loader.js.map +1 -1
  43. package/dist/types/addon.d.ts +6 -0
  44. package/dist/types/addon.d.ts.map +1 -1
  45. package/dist/types/addon.js.map +1 -1
  46. package/dist/types/config.d.ts +18 -85
  47. package/dist/types/config.d.ts.map +1 -1
  48. package/dist/types/config.js +17 -15
  49. package/dist/types/config.js.map +1 -1
  50. package/dist/types/index.d.ts +1 -1
  51. package/dist/types/index.d.ts.map +1 -1
  52. package/dist/types/python.d.ts +2 -0
  53. package/dist/types/python.d.ts.map +1 -1
  54. package/package.json +3 -4
  55. package/prebuilds/darwin-arm64/pynect-prebuild.json +1 -1
  56. package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
  57. package/prebuilds/darwin-x64/pynect-prebuild.json +1 -1
  58. package/prebuilds/darwin-x64/pynect_addon.node +0 -0
  59. package/prebuilds/linux-arm64/pynect-prebuild.json +1 -1
  60. package/prebuilds/linux-arm64/pynect_addon.node +0 -0
  61. package/prebuilds/linux-x64/pynect-prebuild.json +1 -1
  62. package/prebuilds/linux-x64/pynect_addon.node +0 -0
  63. package/prebuilds/win32-arm64/pynect-prebuild.json +1 -1
  64. package/prebuilds/win32-arm64/pynect_addon.node +0 -0
  65. package/prebuilds/win32-x64/pynect-prebuild.json +1 -1
  66. package/prebuilds/win32-x64/pynect_addon.node +0 -0
  67. package/python/pynect_runtime.py +1 -1
  68. package/python/type_inspector.py +12 -0
  69. package/scripts/build-native.js +4 -3
  70. package/scripts/postinstall.js +2 -2
  71. package/scripts/python.js +33 -0
  72. package/scripts/smoke-python-boundary.js +72 -21
  73. package/src/native/callback_bridge.h +11 -0
  74. package/src/native/native-addon-loader.ts +13 -4
  75. package/src/native/pynect_addon.cpp +346 -4
  76. package/src/native/python_bridge.cpp +12 -0
  77. package/src/native/python_bridge.h +7 -0
  78. package/src/native/thread_pool.cpp +62 -55
  79. package/src/native/thread_pool.h +13 -0
  80. package/src/native/type_converter.h +2 -0
  81. package/dist/env/executables.d.ts +0 -3
  82. package/dist/env/executables.d.ts.map +0 -1
  83. package/dist/env/executables.js +0 -14
  84. package/dist/env/executables.js.map +0 -1
  85. package/dist/env/package-installer.d.ts +0 -232
  86. package/dist/env/package-installer.d.ts.map +0 -1
  87. package/dist/env/package-installer.js +0 -615
  88. package/dist/env/package-installer.js.map +0 -1
  89. package/dist/env/pyfig-adapter.d.ts +0 -38
  90. package/dist/env/pyfig-adapter.d.ts.map +0 -1
  91. package/dist/env/pyfig-adapter.js +0 -10
  92. package/dist/env/pyfig-adapter.js.map +0 -1
  93. package/dist/env/python-detector.d.ts +0 -48
  94. package/dist/env/python-detector.d.ts.map +0 -1
  95. package/dist/env/python-detector.js +0 -182
  96. package/dist/env/python-detector.js.map +0 -1
  97. package/dist/env/python-provisioner.d.ts +0 -16
  98. package/dist/env/python-provisioner.d.ts.map +0 -1
  99. package/dist/env/python-provisioner.js +0 -42
  100. package/dist/env/python-provisioner.js.map +0 -1
  101. package/dist/env/venv-manager.d.ts +0 -56
  102. package/dist/env/venv-manager.d.ts.map +0 -1
  103. package/dist/env/venv-manager.js +0 -160
  104. package/dist/env/venv-manager.js.map +0 -1
  105. package/scripts/pyfig.js +0 -142
  106. package/scripts/python-provisioner.js +0 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pynect",
3
- "version": "0.2.7",
3
+ "version": "0.2.10",
4
4
  "packageManager": "bun@1.4.4",
5
5
  "description": "Typed, optionally Pydantic-validated Python calls from Node.js and Bun through embedded CPython.",
6
6
  "main": "dist/index.js",
@@ -29,8 +29,7 @@
29
29
  "scripts/cli.js",
30
30
  "scripts/ensure-native.js",
31
31
  "scripts/postinstall.js",
32
- "scripts/pyfig.js",
33
- "scripts/python-provisioner.js",
32
+ "scripts/python.js",
34
33
  "scripts/smoke-python-boundary.js",
35
34
  "binding.gyp",
36
35
  "README.md",
@@ -86,7 +85,7 @@
86
85
  "node-gyp": "^11.5.0",
87
86
  "node-gyp-build": "^4.8.4",
88
87
  "pacwich": "^0.7.3",
89
- "pyfig": "^0.6.2",
88
+ "pyfig": "^0.7.0",
90
89
  "semver": "^7.8.5"
91
90
  },
92
91
  "devDependencies": {
@@ -6,5 +6,5 @@
6
6
  "pythonAbi": "cp310-abi3",
7
7
  "pythonLibraryName": "libpython3.dylib",
8
8
  "napi": 8,
9
- "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52"
9
+ "nativeFingerprint": "sha256:9cfa5f1c9ffc2988456c3560130f8e2a4bc8df0755003cea0645ccc4f13a1d37"
10
10
  }
@@ -6,5 +6,5 @@
6
6
  "pythonAbi": "cp310-abi3",
7
7
  "pythonLibraryName": "libpython3.dylib",
8
8
  "napi": 8,
9
- "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52"
9
+ "nativeFingerprint": "sha256:9cfa5f1c9ffc2988456c3560130f8e2a4bc8df0755003cea0645ccc4f13a1d37"
10
10
  }
@@ -6,6 +6,6 @@
6
6
  "pythonAbi": "cp310-abi3",
7
7
  "pythonLibraryName": "libpython3.so",
8
8
  "napi": 8,
9
- "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52",
9
+ "nativeFingerprint": "sha256:9cfa5f1c9ffc2988456c3560130f8e2a4bc8df0755003cea0645ccc4f13a1d37",
10
10
  "glibc": "2.39"
11
11
  }
@@ -6,6 +6,6 @@
6
6
  "pythonAbi": "cp310-abi3",
7
7
  "pythonLibraryName": "libpython3.so",
8
8
  "napi": 8,
9
- "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52",
9
+ "nativeFingerprint": "sha256:9cfa5f1c9ffc2988456c3560130f8e2a4bc8df0755003cea0645ccc4f13a1d37",
10
10
  "glibc": "2.35"
11
11
  }
@@ -6,5 +6,5 @@
6
6
  "pythonAbi": "cp310-abi3",
7
7
  "pythonLibraryName": "python3.dll",
8
8
  "napi": 8,
9
- "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52"
9
+ "nativeFingerprint": "sha256:9cfa5f1c9ffc2988456c3560130f8e2a4bc8df0755003cea0645ccc4f13a1d37"
10
10
  }
@@ -6,5 +6,5 @@
6
6
  "pythonAbi": "cp310-abi3",
7
7
  "pythonLibraryName": "python3.dll",
8
8
  "napi": 8,
9
- "nativeFingerprint": "sha256:e375cc859a1ef9a6e8e72f182886c847c2489e9ed5a6017f08d43dd01425bd52"
9
+ "nativeFingerprint": "sha256:9cfa5f1c9ffc2988456c3560130f8e2a4bc8df0755003cea0645ccc4f13a1d37"
10
10
  }
@@ -433,7 +433,7 @@ def _make_error(exc_type: str, message: str, tb: str) -> Dict[str, Any]:
433
433
  # Diagnostic helpers
434
434
 
435
435
  def get_python_info() -> Dict[str, Any]:
436
- """Return basic Python interpreter information (used by PythonDetector)."""
436
+ """Return basic information about the embedded Python interpreter."""
437
437
  import platform
438
438
  return {
439
439
  "version": sys.version,
@@ -11,6 +11,7 @@ Returns a JSON-serializable dict matching RawModuleInspection in TypeGenerator.t
11
11
  from __future__ import annotations
12
12
 
13
13
  import inspect
14
+ import collections.abc
14
15
  import json
15
16
  import importlib
16
17
  import sys
@@ -40,6 +41,9 @@ def _get_annotation(annotation: Any) -> str:
40
41
  return 'None'
41
42
  if isinstance(annotation, str):
42
43
  return annotation
44
+ origin = getattr(annotation, '__origin__', None)
45
+ if origin is collections.abc.Callable:
46
+ return _stringify_generic(annotation)
43
47
  if hasattr(annotation, '__name__'):
44
48
  return annotation.__name__
45
49
  if hasattr(annotation, '__origin__'):
@@ -63,6 +67,14 @@ def _stringify_generic(tp: Any) -> str:
63
67
  parts = [_get_annotation(a) for a in args]
64
68
  return f"Union[{', '.join(parts)}]"
65
69
 
70
+ if origin is collections.abc.Callable:
71
+ if len(args) == 2 and args[0] is Ellipsis:
72
+ return f"Callable[..., {_get_annotation(args[1])}]"
73
+ if args:
74
+ parameters = ', '.join(_get_annotation(a) for a in args[:-1])
75
+ return f"Callable[[{parameters}], {_get_annotation(args[-1])}]"
76
+ return 'Callable'
77
+
66
78
  # typing.Optional[X] is Union[X, None]
67
79
  if len(args) == 2 and type(None) in args:
68
80
  non_none = [a for a in args if a is not type(None)][0]
@@ -3,16 +3,17 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
- const { ensurePynectPython } = require('./pyfig');
6
+ const { preparePython } = require('./python');
7
7
 
8
8
  const ROOT = path.resolve(__dirname, '..');
9
9
 
10
10
  async function main() {
11
11
  const projectRoot = process.env.PYNECT_PROJECT_ROOT || process.env.INIT_CWD || ROOT;
12
- const { python } = await ensurePynectPython({
12
+ const { environment } = await preparePython({
13
13
  projectRoot,
14
- pythonPath: process.env.PYNECT_PYTHON,
14
+ python: process.env.PYNECT_PYTHON,
15
15
  });
16
+ const { python } = environment;
16
17
  const embedding = python.embedding;
17
18
  const stableAbiLibrary = process.platform === 'win32'
18
19
  ? embedding.stableAbiLibraryPath || path.join(embedding.libraryDirectory, 'python3.lib')
@@ -29,8 +29,8 @@ async function main() {
29
29
 
30
30
  const result = await syncProject({ projectRoot });
31
31
  console.log(
32
- `[pynect] Synchronized ${result.plan.requirements.length} Python requirement(s) ` +
33
- `with uv in ${path.dirname(result.plan.stateFile)}.`,
32
+ `[pynect] Prepared ${result.plan.dependencies.length} Python dependency constraint(s) ` +
33
+ `with Pyfig in ${result.environment.python.virtualEnvironment}.`,
34
34
  );
35
35
  }
36
36
 
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { Python, resolveProjectRoot } = require('pyfig');
5
+
6
+ function projectRoot(explicit) {
7
+ const hint = explicit || process.env.PYNECT_PROJECT_ROOT || process.env.INIT_CWD;
8
+ return hint ? path.resolve(hint) : resolveProjectRoot(process.cwd());
9
+ }
10
+
11
+ function createPython(options = {}) {
12
+ return new Python({
13
+ ...options,
14
+ projectRoot: projectRoot(options.projectRoot),
15
+ requireEmbedding: true,
16
+ requireGil: true,
17
+ env: options.env ? { ...process.env, ...options.env } : process.env,
18
+ });
19
+ }
20
+
21
+ async function preparePython(options = {}) {
22
+ const runtime = createPython(options);
23
+ const environment = await runtime.prepare();
24
+ if (!environment.python.embedding) {
25
+ throw new Error(
26
+ `Python ${environment.python.version} at ${environment.python.executable} is missing ` +
27
+ 'Python.h or its CPython embedding library.',
28
+ );
29
+ }
30
+ return { runtime, environment };
31
+ }
32
+
33
+ module.exports = { createPython, preparePython, projectRoot };
@@ -1,48 +1,56 @@
1
1
  'use strict';
2
2
 
3
+ process.env.PYNECT_PREFER_LOCAL ??= '1';
4
+
3
5
  const path = require('path');
4
- const { ensurePynectEnvironment } = require('./pyfig');
6
+ const { preparePython } = require('./python');
5
7
 
6
8
  const ROOT = path.resolve(__dirname, '..');
7
9
 
8
10
  async function main() {
9
- const { client, environment } = await ensurePynectEnvironment({ projectRoot: ROOT });
11
+ const { runtime, environment } = await preparePython({ projectRoot: ROOT });
10
12
  const { python } = environment;
11
13
  const expected = normalize(python.executable);
12
14
  const pynect = require('../dist');
13
15
 
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)'],
16
+ const selected = runtime.uvSync(
17
+ ['run', '--', 'python', '-c', 'import sys; print(sys.executable)'],
23
18
  { encoding: 'utf8' },
24
19
  );
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');
20
+ assertSuccess(selected, 'Pyfig Python uvSync run');
21
+ assertSamePython(selected.stdout.trim(), expected, 'Pyfig Python uvSync run');
30
22
 
31
- const uvPip = client.uvSync(
23
+ const uvPip = runtime.uvSync(
32
24
  ['pip', 'list', '--format', 'json'],
33
25
  { encoding: 'utf8' },
34
26
  );
35
27
  assertSuccess(uvPip, 'PyfigClient.uvSync pip');
36
28
 
37
- const inspector = client.pythonSync(
38
- [path.join(ROOT, 'python', 'type_inspector.py'), 'json'],
29
+ const inspector = runtime.uvSync(
30
+ ['run', '--', 'python', path.join(ROOT, 'python', 'type_inspector.py'), 'json'],
39
31
  { encoding: 'utf8' },
40
32
  );
41
33
  assertSuccess(inspector, 'codegen inspector');
42
34
  JSON.parse(inspector.stdout);
35
+ const callbackInspection = runtime.uvSync(
36
+ ['run', '--', 'python', path.join(ROOT, 'python', 'type_inspector.py'), 'pynect_callback_fixture'],
37
+ {
38
+ encoding: 'utf8',
39
+ env: {
40
+ ...process.env,
41
+ PYTHONPATH: path.join(ROOT, 'tests', 'fixtures'),
42
+ },
43
+ },
44
+ );
45
+ assertSuccess(callbackInspection, 'callback type inspector');
46
+ const inspectedCallback = JSON.parse(callbackInspection.stdout).functions
47
+ .find(item => item.name === 'invoke')?.parameters?.[0]?.type;
48
+ if (inspectedCallback !== 'Callable[[Any], Any]') {
49
+ throw new Error(`Callback annotation was inspected as ${String(inspectedCallback)}.`);
50
+ }
43
51
 
44
- const worker = client.pythonSync(
45
- [path.join(ROOT, 'python', 'isolated_worker.py')],
52
+ const worker = runtime.uvSync(
53
+ ['run', '--', 'python', path.join(ROOT, 'python', 'isolated_worker.py')],
46
54
  {
47
55
  encoding: 'utf8',
48
56
  input: `${JSON.stringify({
@@ -69,10 +77,43 @@ async function main() {
69
77
  projectRoot: ROOT,
70
78
  allowUnsafeCode: true,
71
79
  logLevel: 'error',
80
+ pythonModulePaths: [path.join(ROOT, 'tests', 'fixtures')],
81
+ threadPool: { size: 1 },
82
+ callbacks: { maxDepth: 2 },
72
83
  });
73
84
  try {
74
85
  const embedded = await pynect.py.eval('__import__("sys").executable');
75
86
  assertSamePython(embedded, expected, 'embedded CPython');
87
+ const fixture = await pynect.importModule('pynect_callback_fixture');
88
+ const asyncCallback = async value => {
89
+ await Promise.resolve();
90
+ return value * 3;
91
+ };
92
+ if (await fixture.invoke(asyncCallback, 4) !== 12) {
93
+ throw new Error('Async JavaScript callback returned the wrong value.');
94
+ }
95
+ await fixture.retain(asyncCallback);
96
+ await assertRejects(
97
+ () => fixture.invoke_retained(4),
98
+ /callback.*(?:disposed|registered)|(?:disposed|registered).*callback/i,
99
+ 'settled callback lifetime',
100
+ );
101
+
102
+ const reentrantCallback = async value => fixture.double(value);
103
+ if (await fixture.invoke(reentrantCallback, 5) !== 10) {
104
+ throw new Error('Reentrant JavaScript callback returned the wrong value.');
105
+ }
106
+
107
+ let recursiveCallback;
108
+ recursiveCallback = async depth => {
109
+ if (depth <= 0) return 0;
110
+ return fixture.invoke(recursiveCallback, depth - 1);
111
+ };
112
+ await assertRejects(
113
+ () => fixture.invoke(recursiveCallback, 2),
114
+ /recursion depth 3 exceeds the configured maximum of 2/,
115
+ 'callback recursion limit',
116
+ );
76
117
  } finally {
77
118
  await pynect.shutdown();
78
119
  }
@@ -80,6 +121,16 @@ async function main() {
80
121
  console.log(`Python ${python.version} boundary verified: ${python.executable}`);
81
122
  }
82
123
 
124
+ async function assertRejects(action, pattern, label) {
125
+ try {
126
+ await action();
127
+ } catch (error) {
128
+ if (pattern.test(String(error?.message || error))) return;
129
+ throw new Error(`${label} failed with an unexpected error: ${error?.message || error}`);
130
+ }
131
+ throw new Error(`${label} did not reject.`);
132
+ }
133
+
83
134
  function assertSuccess(result, label) {
84
135
  if (result.status !== 0 || result.error) {
85
136
  throw new Error(`${label} failed: ${String(result.stderr || result.error?.message || result.status)}`);
@@ -0,0 +1,11 @@
1
+ #pragma once
2
+
3
+ #include <Python.h>
4
+ #include <cstdint>
5
+
6
+ namespace pynect {
7
+
8
+ /** Create a Python callable backed by a registered JavaScript callback. */
9
+ PyObject* createPythonCallback(uint32_t callbackId);
10
+
11
+ } // namespace pynect
@@ -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
- try {
27
- return loadMatchingLocalAddon(options);
28
- } catch (error) {
29
- initialError = new AggregateError([initialError, error], 'No compatible native addon loaded.');
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