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,575 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nodepyx_runtime.py
|
|
3
|
+
=================
|
|
4
|
+
Python-side runtime helper that is imported by PyRuntime._initialize() via the
|
|
5
|
+
C++ N-API addon. This module provides the bridge between the embedded CPython
|
|
6
|
+
interpreter and the JavaScript layer.
|
|
7
|
+
|
|
8
|
+
Responsibilities
|
|
9
|
+
────────────────
|
|
10
|
+
• Initialize Python-side state (sys.path, codec registration, etc.)
|
|
11
|
+
• Expose object reference management (id → object registry)
|
|
12
|
+
• Resolve attribute chains (obj, ["attr1", "attr2", "method"])
|
|
13
|
+
• Call callables with positional / keyword arguments
|
|
14
|
+
• Build and iterate Python iterators
|
|
15
|
+
• Set attributes on Python objects
|
|
16
|
+
• Serialize return values to the wire format expected by Serializer.ts
|
|
17
|
+
• Handle exceptions and forward structured error information
|
|
18
|
+
• Provide module introspection used by TypeGenerator.ts
|
|
19
|
+
|
|
20
|
+
Thread safety
|
|
21
|
+
─────────────
|
|
22
|
+
All functions in this module are called from the C++ thread pool while the
|
|
23
|
+
GIL is held. Because the GIL serialises access there is no need for
|
|
24
|
+
additional locking inside Python.
|
|
25
|
+
|
|
26
|
+
Wire protocol
|
|
27
|
+
─────────────
|
|
28
|
+
The serialization module (serializer.py) is imported lazily and its
|
|
29
|
+
``serialize`` function is the single exit point for Python values heading
|
|
30
|
+
back to JavaScript. The C++ layer receives a Python dict that matches the
|
|
31
|
+
SerializedValue TypeScript interface.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import sys
|
|
37
|
+
import os
|
|
38
|
+
import importlib
|
|
39
|
+
import inspect
|
|
40
|
+
import traceback
|
|
41
|
+
import weakref
|
|
42
|
+
import threading
|
|
43
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
44
|
+
|
|
45
|
+
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
nodepyx_RUNTIME_VERSION = "1.0.0"
|
|
48
|
+
|
|
49
|
+
# ─── Object Registry ──────────────────────────────────────────────────────────
|
|
50
|
+
# Maps integer object-ids that cross the JS/Python boundary to live Python
|
|
51
|
+
# objects. We use a plain dict of strong references here because Python
|
|
52
|
+
# objects must NOT be garbage-collected while JS holds a PyProxy to them.
|
|
53
|
+
# The C++ addon calls ``release_object`` when the corresponding JS Proxy is
|
|
54
|
+
# garbage-collected via FinalizationRegistry.
|
|
55
|
+
|
|
56
|
+
_registry: Dict[int, Any] = {}
|
|
57
|
+
_registry_lock = threading.Lock()
|
|
58
|
+
_next_id = 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _register(obj: Any) -> int:
|
|
62
|
+
"""Store *obj* in the registry and return its numeric id."""
|
|
63
|
+
global _next_id
|
|
64
|
+
with _registry_lock:
|
|
65
|
+
oid = _next_id
|
|
66
|
+
_next_id += 1
|
|
67
|
+
_registry[oid] = obj
|
|
68
|
+
return oid
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _lookup(oid: int) -> Any:
|
|
72
|
+
"""Retrieve the object for *oid* or raise KeyError."""
|
|
73
|
+
with _registry_lock:
|
|
74
|
+
return _registry[oid]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def release_object(oid: int) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Remove the object associated with *oid* from the registry.
|
|
80
|
+
Called by the C++ FinalizationRegistry callback when the JS Proxy dies.
|
|
81
|
+
"""
|
|
82
|
+
with _registry_lock:
|
|
83
|
+
_registry.pop(oid, None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def registry_size() -> int:
|
|
87
|
+
"""Return the number of live objects in the registry (diagnostic)."""
|
|
88
|
+
with _registry_lock:
|
|
89
|
+
return len(_registry)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ─── Initialization ────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
_initialized = False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def initialize(extra_paths: Optional[List[str]] = None) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Called once by PyRuntime._initialize().
|
|
100
|
+
|
|
101
|
+
• Ensures the nodepyx python/ directory is on sys.path.
|
|
102
|
+
• Registers the msgpack extension codec (if msgpack is available).
|
|
103
|
+
• Sets UTF-8 as the default stdout/stderr encoding.
|
|
104
|
+
• Performs any one-time setup required by the serializer.
|
|
105
|
+
"""
|
|
106
|
+
global _initialized
|
|
107
|
+
if _initialized:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# ── Make sure the python/ directory is importable ──────────────────────
|
|
111
|
+
runtime_dir = os.path.dirname(os.path.abspath(__file__))
|
|
112
|
+
if runtime_dir not in sys.path:
|
|
113
|
+
sys.path.insert(0, runtime_dir)
|
|
114
|
+
|
|
115
|
+
if extra_paths:
|
|
116
|
+
for p in extra_paths:
|
|
117
|
+
if p not in sys.path:
|
|
118
|
+
sys.path.insert(0, p)
|
|
119
|
+
|
|
120
|
+
# ── Force UTF-8 output (avoids encoding errors with Python 3.6) ───────
|
|
121
|
+
try:
|
|
122
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
123
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace') # type: ignore[attr-defined]
|
|
124
|
+
if hasattr(sys.stderr, 'reconfigure'):
|
|
125
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace') # type: ignore[attr-defined]
|
|
126
|
+
except Exception:
|
|
127
|
+
pass # non-fatal
|
|
128
|
+
|
|
129
|
+
_initialized = True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ─── Module import ─────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
def import_module(module_name: str) -> Dict[str, Any]:
|
|
135
|
+
"""
|
|
136
|
+
Import *module_name* and return a wire-format dict.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
{"type": "python_object", "objectId": <int>}
|
|
141
|
+
on success, or an error dict on failure.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
mod = importlib.import_module(module_name)
|
|
145
|
+
oid = _register(mod)
|
|
146
|
+
return {"type": "python_object", "objectId": oid}
|
|
147
|
+
except ImportError as exc:
|
|
148
|
+
return _make_error("ImportError", str(exc), traceback.format_exc())
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ─── Attribute resolution ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
def resolve_attribute_path(oid: int, path: List[str]) -> Dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Starting from the registered object *oid*, follow the attribute chain
|
|
158
|
+
described by *path* and return the final value serialized for the wire.
|
|
159
|
+
|
|
160
|
+
Example: oid → pandas module, path=["DataFrame","from_dict"]
|
|
161
|
+
Returns a reference to the unbound method DataFrame.from_dict.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
obj = _lookup(oid)
|
|
165
|
+
for attr in path:
|
|
166
|
+
obj = getattr(obj, attr)
|
|
167
|
+
return _serialize_result(obj)
|
|
168
|
+
except AttributeError as exc:
|
|
169
|
+
return _make_error("AttributeError", str(exc), traceback.format_exc())
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_object_value(oid: int) -> Dict[str, Any]:
|
|
175
|
+
"""Serialize the registered object *oid* for transfer to JS."""
|
|
176
|
+
try:
|
|
177
|
+
obj = _lookup(oid)
|
|
178
|
+
return _serialize_result(obj)
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def set_attribute(oid: int, attr_name: str, serialized_value: Dict[str, Any]) -> Dict[str, Any]:
|
|
184
|
+
"""
|
|
185
|
+
Set ``obj.attr_name = value`` where *obj* is the registered object for
|
|
186
|
+
*oid* and *value* is deserialized from *serialized_value*.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
from serializer import deserialize as _deser # lazy import
|
|
190
|
+
obj = _lookup(oid)
|
|
191
|
+
value = _deser(serialized_value)
|
|
192
|
+
setattr(obj, attr_name, value)
|
|
193
|
+
return {"type": "null", "data": "null"}
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ─── Function / method call ────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def call_function(
|
|
201
|
+
oid: int,
|
|
202
|
+
path: List[str],
|
|
203
|
+
serialized_args: List[Dict[str, Any]],
|
|
204
|
+
serialized_kwargs: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
205
|
+
) -> Dict[str, Any]:
|
|
206
|
+
"""
|
|
207
|
+
Resolve ``obj.path[0].path[1].…`` starting from *oid*, then call it with
|
|
208
|
+
the provided positional arguments (deserialized from *serialized_args*) and
|
|
209
|
+
keyword arguments (deserialized from *serialized_kwargs*).
|
|
210
|
+
|
|
211
|
+
All arguments are deserialized via serializer.deserialize().
|
|
212
|
+
The return value is serialized via _serialize_result().
|
|
213
|
+
"""
|
|
214
|
+
from serializer import deserialize as _deser # lazy import
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
obj = _lookup(oid)
|
|
218
|
+
for attr in path:
|
|
219
|
+
obj = getattr(obj, attr)
|
|
220
|
+
|
|
221
|
+
if not callable(obj):
|
|
222
|
+
return _make_error(
|
|
223
|
+
"TypeError",
|
|
224
|
+
f"Object at path {path!r} is not callable (got {type(obj).__name__})",
|
|
225
|
+
""
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
args = [_deser(a) for a in serialized_args]
|
|
229
|
+
kwargs = {k: _deser(v) for k, v in (serialized_kwargs or {}).items()}
|
|
230
|
+
|
|
231
|
+
result = obj(*args, **kwargs)
|
|
232
|
+
return _serialize_result(result)
|
|
233
|
+
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def eval_code(code: str, globals_oid: Optional[int] = None) -> Dict[str, Any]:
|
|
239
|
+
"""
|
|
240
|
+
Execute *code* with ``eval()`` (single expression) or ``exec()``
|
|
241
|
+
(statements) and return the result.
|
|
242
|
+
|
|
243
|
+
If *globals_oid* is given, use the registered dict as the global namespace.
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
g: Dict[str, Any] = _lookup(globals_oid) if globals_oid is not None else {}
|
|
247
|
+
# Try eval first for expressions
|
|
248
|
+
try:
|
|
249
|
+
result = eval(code, g) # noqa: S307
|
|
250
|
+
except SyntaxError:
|
|
251
|
+
exec(code, g) # noqa: S102
|
|
252
|
+
result = None
|
|
253
|
+
return _serialize_result(result)
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_code(code: str, namespace: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Execute *code* as a statement block (exec) and return the namespace.
|
|
261
|
+
Useful for multi-line setup scripts.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
ns: Dict[str, Any] = namespace or {}
|
|
265
|
+
exec(code, ns) # noqa: S102
|
|
266
|
+
return {"type": "null", "data": "null"}
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ─── Iterator support ──────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
_iterators: Dict[int, Any] = {}
|
|
274
|
+
_iter_lock = threading.Lock()
|
|
275
|
+
_iter_next_id = 1
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def create_iterator(oid: int, path: List[str]) -> Dict[str, Any]:
|
|
279
|
+
"""
|
|
280
|
+
Build a Python iterator from the object at oid.path and register it.
|
|
281
|
+
Returns {"iteratorId": <int>}.
|
|
282
|
+
"""
|
|
283
|
+
global _iter_next_id
|
|
284
|
+
try:
|
|
285
|
+
obj = _lookup(oid)
|
|
286
|
+
for attr in path:
|
|
287
|
+
obj = getattr(obj, attr)
|
|
288
|
+
it = iter(obj)
|
|
289
|
+
with _iter_lock:
|
|
290
|
+
iter_id = _iter_next_id
|
|
291
|
+
_iter_next_id += 1
|
|
292
|
+
_iterators[iter_id] = it
|
|
293
|
+
return {"iteratorId": iter_id}
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def iterator_next(iter_id: int) -> Dict[str, Any]:
|
|
299
|
+
"""
|
|
300
|
+
Advance the iterator and return the next value.
|
|
301
|
+
Returns {"done": true} when exhausted.
|
|
302
|
+
"""
|
|
303
|
+
with _iter_lock:
|
|
304
|
+
it = _iterators.get(iter_id)
|
|
305
|
+
if it is None:
|
|
306
|
+
return _make_error("RuntimeError", f"No iterator with id {iter_id}", "")
|
|
307
|
+
try:
|
|
308
|
+
value = next(it)
|
|
309
|
+
return {"done": False, "value": _serialize_result(value)}
|
|
310
|
+
except StopIteration:
|
|
311
|
+
return {"done": True}
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def destroy_iterator(iter_id: int) -> None:
|
|
317
|
+
"""Release the iterator registered under *iter_id*."""
|
|
318
|
+
with _iter_lock:
|
|
319
|
+
_iterators.pop(iter_id, None)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ─── Module introspection ──────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
def inspect_module(module_name: str) -> List[Dict[str, Any]]:
|
|
325
|
+
"""
|
|
326
|
+
Introspect *module_name* and return a list of RawModuleInspection-shaped
|
|
327
|
+
dicts. This is consumed by TypeGenerator.ts to produce .d.ts stubs.
|
|
328
|
+
|
|
329
|
+
Delegates to type_inspector.inspect_module if available.
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
# Prefer the dedicated inspector
|
|
333
|
+
from type_inspector import inspect_module as _inspect # type: ignore
|
|
334
|
+
return _inspect(module_name)
|
|
335
|
+
except ImportError:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
# Minimal fallback
|
|
339
|
+
try:
|
|
340
|
+
mod = importlib.import_module(module_name)
|
|
341
|
+
except ImportError as exc:
|
|
342
|
+
return [{"error": str(exc)}]
|
|
343
|
+
|
|
344
|
+
results: List[Dict[str, Any]] = []
|
|
345
|
+
for name, obj in inspect.getmembers(mod):
|
|
346
|
+
if name.startswith('_'):
|
|
347
|
+
continue
|
|
348
|
+
entry: Dict[str, Any] = {
|
|
349
|
+
"name": name,
|
|
350
|
+
"docstring": inspect.getdoc(obj) or "",
|
|
351
|
+
}
|
|
352
|
+
if inspect.isfunction(obj) or inspect.isbuiltin(obj):
|
|
353
|
+
entry["type"] = "function"
|
|
354
|
+
try:
|
|
355
|
+
sig = inspect.signature(obj)
|
|
356
|
+
entry["parameters"] = [
|
|
357
|
+
{
|
|
358
|
+
"name": pname,
|
|
359
|
+
"type": _annotation_to_str(p.annotation),
|
|
360
|
+
"optional": p.default is not inspect.Parameter.empty,
|
|
361
|
+
}
|
|
362
|
+
for pname, p in sig.parameters.items()
|
|
363
|
+
if pname != "self"
|
|
364
|
+
]
|
|
365
|
+
entry["returnType"] = _annotation_to_str(sig.return_annotation)
|
|
366
|
+
except (ValueError, TypeError):
|
|
367
|
+
entry["parameters"] = []
|
|
368
|
+
entry["returnType"] = "Any"
|
|
369
|
+
elif inspect.isclass(obj):
|
|
370
|
+
entry["type"] = "class"
|
|
371
|
+
entry["properties"] = {
|
|
372
|
+
attr: "Any"
|
|
373
|
+
for attr in dir(obj)
|
|
374
|
+
if not attr.startswith('_')
|
|
375
|
+
}
|
|
376
|
+
else:
|
|
377
|
+
entry["type"] = "value"
|
|
378
|
+
entry["valueType"] = type(obj).__name__
|
|
379
|
+
results.append(entry)
|
|
380
|
+
return results
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _annotation_to_str(ann: Any) -> str:
|
|
384
|
+
if ann is inspect.Parameter.empty:
|
|
385
|
+
return "Any"
|
|
386
|
+
if hasattr(ann, '__name__'):
|
|
387
|
+
return ann.__name__
|
|
388
|
+
return str(ann).replace("typing.", "")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ─── Serialization helpers ────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
def _serialize_result(obj: Any) -> Dict[str, Any]:
|
|
394
|
+
"""
|
|
395
|
+
Serialize *obj* to a wire dict.
|
|
396
|
+
|
|
397
|
+
Attempts to use the full serializer. Falls back to a simple primitive
|
|
398
|
+
check if the serializer is not yet available (early boot).
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
from serializer import serialize # lazy import
|
|
402
|
+
return serialize(obj)
|
|
403
|
+
except ImportError:
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
# Minimal primitive fallback (used during early initialization)
|
|
407
|
+
import json
|
|
408
|
+
if obj is None:
|
|
409
|
+
return {"format": "json", "data": "null"}
|
|
410
|
+
if isinstance(obj, (bool, int, float, str)):
|
|
411
|
+
return {"format": "json", "data": json.dumps(obj)}
|
|
412
|
+
if isinstance(obj, (list, dict)):
|
|
413
|
+
try:
|
|
414
|
+
return {"format": "json", "data": json.dumps(obj)}
|
|
415
|
+
except (TypeError, ValueError):
|
|
416
|
+
pass
|
|
417
|
+
# Non-serializable → register as Python object reference
|
|
418
|
+
oid = _register(obj)
|
|
419
|
+
return {"format": "python_ref", "data": None, "metadata": {"objectId": oid}}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _make_error(exc_type: str, message: str, tb: str) -> Dict[str, Any]:
|
|
423
|
+
"""Build a structured error dict forwarded to ErrorTranslator.ts."""
|
|
424
|
+
return {
|
|
425
|
+
"error": {
|
|
426
|
+
"type": exc_type,
|
|
427
|
+
"message": message,
|
|
428
|
+
"traceback": tb,
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ─── Diagnostic helpers ────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
def get_python_info() -> Dict[str, Any]:
|
|
436
|
+
"""Return basic Python interpreter information (used by PythonDetector)."""
|
|
437
|
+
import platform
|
|
438
|
+
return {
|
|
439
|
+
"version": sys.version,
|
|
440
|
+
"version_info": list(sys.version_info[:3]),
|
|
441
|
+
"executable": sys.executable,
|
|
442
|
+
"prefix": sys.prefix,
|
|
443
|
+
"platform": sys.platform,
|
|
444
|
+
"machine": platform.machine(),
|
|
445
|
+
"is_venv": hasattr(sys, 'real_prefix') or (
|
|
446
|
+
hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
|
|
447
|
+
),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def get_installed_packages() -> List[Dict[str, str]]:
|
|
452
|
+
"""
|
|
453
|
+
Return a list of {name, version} dicts for all installed packages.
|
|
454
|
+
Uses importlib.metadata (Python 3.8+) or pkg_resources as fallback.
|
|
455
|
+
"""
|
|
456
|
+
try:
|
|
457
|
+
import importlib.metadata as imeta
|
|
458
|
+
return [
|
|
459
|
+
{"name": d.metadata['Name'], "version": d.version}
|
|
460
|
+
for d in imeta.distributions()
|
|
461
|
+
]
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
464
|
+
try:
|
|
465
|
+
import pkg_resources
|
|
466
|
+
return [
|
|
467
|
+
{"name": pkg.project_name, "version": pkg.version}
|
|
468
|
+
for pkg in pkg_resources.working_set
|
|
469
|
+
]
|
|
470
|
+
except Exception:
|
|
471
|
+
return []
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def check_module_importable(module_name: str) -> bool:
|
|
475
|
+
"""Return True if *module_name* can be imported without error."""
|
|
476
|
+
try:
|
|
477
|
+
importlib.import_module(module_name)
|
|
478
|
+
return True
|
|
479
|
+
except ImportError:
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def get_module_version(module_name: str) -> Optional[str]:
|
|
484
|
+
"""Return the installed version of *module_name*, or None."""
|
|
485
|
+
try:
|
|
486
|
+
import importlib.metadata as imeta
|
|
487
|
+
return imeta.version(module_name)
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
try:
|
|
491
|
+
mod = importlib.import_module(module_name)
|
|
492
|
+
for attr in ('__version__', 'VERSION', 'version'):
|
|
493
|
+
v = getattr(mod, attr, None)
|
|
494
|
+
if v is not None:
|
|
495
|
+
return str(v)
|
|
496
|
+
except Exception:
|
|
497
|
+
pass
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ─── Context / namespace management ──────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
def create_namespace(init_code: Optional[str] = None) -> Dict[str, Any]:
|
|
504
|
+
"""
|
|
505
|
+
Create an isolated execution namespace (analogous to PyContext.ts).
|
|
506
|
+
Returns {"namespaceId": <oid>}.
|
|
507
|
+
"""
|
|
508
|
+
ns: Dict[str, Any] = {"__builtins__": __builtins__}
|
|
509
|
+
if init_code:
|
|
510
|
+
exec(init_code, ns) # noqa: S102
|
|
511
|
+
oid = _register(ns)
|
|
512
|
+
return {"namespaceId": oid}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def exec_in_namespace(namespace_oid: int, code: str) -> Dict[str, Any]:
|
|
516
|
+
"""Execute *code* inside the namespace registered as *namespace_oid*."""
|
|
517
|
+
try:
|
|
518
|
+
ns = _lookup(namespace_oid)
|
|
519
|
+
exec(code, ns) # noqa: S102
|
|
520
|
+
return {"type": "null", "data": "null"}
|
|
521
|
+
except Exception as exc:
|
|
522
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def eval_in_namespace(namespace_oid: int, expression: str) -> Dict[str, Any]:
|
|
526
|
+
"""Evaluate *expression* inside the namespace and return the result."""
|
|
527
|
+
try:
|
|
528
|
+
ns = _lookup(namespace_oid)
|
|
529
|
+
result = eval(expression, ns) # noqa: S307
|
|
530
|
+
return _serialize_result(result)
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def get_namespace_keys(namespace_oid: int) -> List[str]:
|
|
536
|
+
"""Return the list of names defined in the namespace (excluding builtins)."""
|
|
537
|
+
try:
|
|
538
|
+
ns = _lookup(namespace_oid)
|
|
539
|
+
return [k for k in ns if not k.startswith('__')]
|
|
540
|
+
except Exception:
|
|
541
|
+
return []
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ─── Async / generator utilities ─────────────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
def create_generator(oid: int, path: List[str], *args: Any) -> Dict[str, Any]:
|
|
547
|
+
"""
|
|
548
|
+
Call the callable at oid.path with *args* and expect it to be a generator
|
|
549
|
+
function. Registers the resulting generator and returns its id.
|
|
550
|
+
"""
|
|
551
|
+
try:
|
|
552
|
+
obj = _lookup(oid)
|
|
553
|
+
for attr in path:
|
|
554
|
+
obj = getattr(obj, attr)
|
|
555
|
+
gen = obj(*args)
|
|
556
|
+
if not hasattr(gen, '__next__'):
|
|
557
|
+
return _make_error("TypeError", "Not a generator function", "")
|
|
558
|
+
oid_gen = _register(gen)
|
|
559
|
+
return {"generatorId": oid_gen}
|
|
560
|
+
except Exception as exc:
|
|
561
|
+
return _make_error(type(exc).__name__, str(exc), traceback.format_exc())
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# ─── Cleanup ──────────────────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
def shutdown() -> None:
|
|
567
|
+
"""
|
|
568
|
+
Called by PyRuntime.shutdown() before Py_Finalize().
|
|
569
|
+
Clears the object registry and iterator table to allow GC to run cleanly.
|
|
570
|
+
"""
|
|
571
|
+
with _registry_lock:
|
|
572
|
+
_registry.clear()
|
|
573
|
+
with _iter_lock:
|
|
574
|
+
_iterators.clear()
|
|
575
|
+
|