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.
Files changed (184) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +399 -0
  3. package/binding.gyp +73 -0
  4. package/dist/core/PyCallable.d.ts +65 -0
  5. package/dist/core/PyCallable.d.ts.map +1 -0
  6. package/dist/core/PyCallable.js +109 -0
  7. package/dist/core/PyCallable.js.map +1 -0
  8. package/dist/core/PyContext.d.ts +76 -0
  9. package/dist/core/PyContext.d.ts.map +1 -0
  10. package/dist/core/PyContext.js +228 -0
  11. package/dist/core/PyContext.js.map +1 -0
  12. package/dist/core/PyIterator.d.ts +84 -0
  13. package/dist/core/PyIterator.d.ts.map +1 -0
  14. package/dist/core/PyIterator.js +243 -0
  15. package/dist/core/PyIterator.js.map +1 -0
  16. package/dist/core/PyModule.d.ts +55 -0
  17. package/dist/core/PyModule.d.ts.map +1 -0
  18. package/dist/core/PyModule.js +172 -0
  19. package/dist/core/PyModule.js.map +1 -0
  20. package/dist/core/PyProxy.d.ts +65 -0
  21. package/dist/core/PyProxy.d.ts.map +1 -0
  22. package/dist/core/PyProxy.js +483 -0
  23. package/dist/core/PyProxy.js.map +1 -0
  24. package/dist/core/PyRuntime.d.ts +105 -0
  25. package/dist/core/PyRuntime.d.ts.map +1 -0
  26. package/dist/core/PyRuntime.js +438 -0
  27. package/dist/core/PyRuntime.js.map +1 -0
  28. package/dist/env/CondaManager.d.ts +118 -0
  29. package/dist/env/CondaManager.d.ts.map +1 -0
  30. package/dist/env/CondaManager.js +401 -0
  31. package/dist/env/CondaManager.js.map +1 -0
  32. package/dist/env/PackageInstaller.d.ts +233 -0
  33. package/dist/env/PackageInstaller.d.ts.map +1 -0
  34. package/dist/env/PackageInstaller.js +609 -0
  35. package/dist/env/PackageInstaller.js.map +1 -0
  36. package/dist/env/PythonDetector.d.ts +103 -0
  37. package/dist/env/PythonDetector.d.ts.map +1 -0
  38. package/dist/env/PythonDetector.js +381 -0
  39. package/dist/env/PythonDetector.js.map +1 -0
  40. package/dist/env/VenvManager.d.ts +117 -0
  41. package/dist/env/VenvManager.d.ts.map +1 -0
  42. package/dist/env/VenvManager.js +331 -0
  43. package/dist/env/VenvManager.js.map +1 -0
  44. package/dist/index.d.ts +169 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +393 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/plugins/Plugin.interface.d.ts +41 -0
  49. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  50. package/dist/plugins/Plugin.interface.js +12 -0
  51. package/dist/plugins/Plugin.interface.js.map +1 -0
  52. package/dist/plugins/PluginManager.d.ts +26 -0
  53. package/dist/plugins/PluginManager.d.ts.map +1 -0
  54. package/dist/plugins/PluginManager.js +174 -0
  55. package/dist/plugins/PluginManager.js.map +1 -0
  56. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  57. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  58. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  59. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  60. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  61. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  62. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  63. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  64. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  65. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  66. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  67. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  68. package/dist/plugins/index.d.ts +7 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +12 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  73. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  74. package/dist/serialization/DataFrameBridge.js +355 -0
  75. package/dist/serialization/DataFrameBridge.js.map +1 -0
  76. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  77. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  78. package/dist/serialization/MsgPackSerializer.js +242 -0
  79. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  80. package/dist/serialization/NumpyBridge.d.ts +96 -0
  81. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  82. package/dist/serialization/NumpyBridge.js +323 -0
  83. package/dist/serialization/NumpyBridge.js.map +1 -0
  84. package/dist/serialization/Serializer.d.ts +78 -0
  85. package/dist/serialization/Serializer.d.ts.map +1 -0
  86. package/dist/serialization/Serializer.js +281 -0
  87. package/dist/serialization/Serializer.js.map +1 -0
  88. package/dist/types/PythonTypeMapper.d.ts +87 -0
  89. package/dist/types/PythonTypeMapper.d.ts.map +1 -0
  90. package/dist/types/PythonTypeMapper.js +449 -0
  91. package/dist/types/PythonTypeMapper.js.map +1 -0
  92. package/dist/types/StubCache.d.ts +109 -0
  93. package/dist/types/StubCache.d.ts.map +1 -0
  94. package/dist/types/StubCache.js +333 -0
  95. package/dist/types/StubCache.js.map +1 -0
  96. package/dist/types/TypeGenerator.d.ts +139 -0
  97. package/dist/types/TypeGenerator.d.ts.map +1 -0
  98. package/dist/types/TypeGenerator.js +372 -0
  99. package/dist/types/TypeGenerator.js.map +1 -0
  100. package/dist/types/addon.d.ts +114 -0
  101. package/dist/types/addon.d.ts.map +1 -0
  102. package/dist/types/addon.js +32 -0
  103. package/dist/types/addon.js.map +1 -0
  104. package/dist/types/config.d.ts +175 -0
  105. package/dist/types/config.d.ts.map +1 -0
  106. package/dist/types/config.js +35 -0
  107. package/dist/types/config.js.map +1 -0
  108. package/dist/types/index.d.ts +10 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/index.js +12 -0
  111. package/dist/types/index.js.map +1 -0
  112. package/dist/types/python.d.ts +235 -0
  113. package/dist/types/python.d.ts.map +1 -0
  114. package/dist/types/python.js +7 -0
  115. package/dist/types/python.js.map +1 -0
  116. package/dist/utils/ErrorTranslator.d.ts +83 -0
  117. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  118. package/dist/utils/ErrorTranslator.js +210 -0
  119. package/dist/utils/ErrorTranslator.js.map +1 -0
  120. package/dist/utils/Logger.d.ts +27 -0
  121. package/dist/utils/Logger.d.ts.map +1 -0
  122. package/dist/utils/Logger.js +115 -0
  123. package/dist/utils/Logger.js.map +1 -0
  124. package/dist/utils/MemoryMonitor.d.ts +44 -0
  125. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  126. package/dist/utils/MemoryMonitor.js +143 -0
  127. package/dist/utils/MemoryMonitor.js.map +1 -0
  128. package/package.json +177 -0
  129. package/python/error_handler.py +433 -0
  130. package/python/nodepyx_runtime.py +575 -0
  131. package/python/serializer.py +379 -0
  132. package/python/type_inspector.py +288 -0
  133. package/scripts/build-native.js +68 -0
  134. package/scripts/download-prebuilds.js +99 -0
  135. package/scripts/generate-stubs.js +405 -0
  136. package/scripts/install.js +260 -0
  137. package/src/core/PyCallable.ts +137 -0
  138. package/src/core/PyContext.ts +296 -0
  139. package/src/core/PyIterator.ts +294 -0
  140. package/src/core/PyModule.ts +194 -0
  141. package/src/core/PyProxy.ts +605 -0
  142. package/src/core/PyRuntime.ts +504 -0
  143. package/src/env/CondaManager.ts +451 -0
  144. package/src/env/PackageInstaller.ts +738 -0
  145. package/src/env/PythonDetector.ts +414 -0
  146. package/src/env/VenvManager.ts +396 -0
  147. package/src/index.ts +425 -0
  148. package/src/native/gil_guard.cpp +26 -0
  149. package/src/native/gil_guard.h +175 -0
  150. package/src/native/nodepyx_addon.cpp +886 -0
  151. package/src/native/python_bridge.cpp +790 -0
  152. package/src/native/python_bridge.h +257 -0
  153. package/src/native/thread_pool.cpp +336 -0
  154. package/src/native/thread_pool.h +175 -0
  155. package/src/native/type_converter.cpp +901 -0
  156. package/src/native/type_converter.h +272 -0
  157. package/src/nextjs/PyProvider.tsx +123 -0
  158. package/src/nextjs/index.ts +21 -0
  159. package/src/nextjs/usePython.ts +106 -0
  160. package/src/nextjs/withnodepyx.ts +88 -0
  161. package/src/plugins/Plugin.interface.ts +51 -0
  162. package/src/plugins/PluginManager.ts +155 -0
  163. package/src/plugins/builtin/NumpyPlugin.ts +36 -0
  164. package/src/plugins/builtin/PandasPlugin.ts +49 -0
  165. package/src/plugins/builtin/TorchPlugin.ts +56 -0
  166. package/src/plugins/index.ts +7 -0
  167. package/src/serialization/DataFrameBridge.ts +398 -0
  168. package/src/serialization/MsgPackSerializer.ts +220 -0
  169. package/src/serialization/NumpyBridge.ts +332 -0
  170. package/src/serialization/Serializer.ts +320 -0
  171. package/src/types/PythonTypeMapper.ts +495 -0
  172. package/src/types/StubCache.ts +340 -0
  173. package/src/types/TypeGenerator.ts +491 -0
  174. package/src/types/addon.ts +170 -0
  175. package/src/types/config.ts +226 -0
  176. package/src/types/index.ts +55 -0
  177. package/src/types/python.ts +309 -0
  178. package/src/types/stubs/numpy.d.ts +441 -0
  179. package/src/types/stubs/pandas.d.ts +575 -0
  180. package/src/types/stubs/sklearn.d.ts +728 -0
  181. package/src/types/stubs/torch.d.ts +694 -0
  182. package/src/utils/ErrorTranslator.ts +220 -0
  183. package/src/utils/Logger.ts +119 -0
  184. 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
+