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
package/package.json ADDED
@@ -0,0 +1,177 @@
1
+ {
2
+ "name": "nodepyx",
3
+ "version": "1.0.0",
4
+ "description": "Run Python libraries from Node.js as if they were Native — embed CPython in-process with full TypeScript types, Proxy-based API, async/await support, and Next.js integration",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./next": {
15
+ "import": "./dist/nextjs/index.mjs",
16
+ "require": "./dist/nextjs/index.js",
17
+ "types": "./dist/nextjs/index.d.ts"
18
+ },
19
+ "./types": {
20
+ "import": "./dist/types/index.mjs",
21
+ "require": "./dist/types/index.js",
22
+ "types": "./dist/types/index.d.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist/",
27
+ "src/",
28
+ "python/",
29
+ "prebuilds/",
30
+ "binding.gyp",
31
+ "scripts/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "npm run build:ts",
37
+ "build:ts": "tsc -p tsconfig.json",
38
+ "build:ts:esm": "tsc -p tsconfig.esm.json",
39
+ "build:native": "node-gyp rebuild",
40
+ "build:native:debug": "node-gyp rebuild --debug",
41
+ "postinstall": "node scripts/install.js",
42
+ "prepack": "npm run build",
43
+ "generate-stubs": "node scripts/generate-stubs.js",
44
+ "generate-stubs:all": "node scripts/generate-stubs.js --all",
45
+ "clean": "rm -rf dist build .nodepyx",
46
+ "clean:all": "rm -rf dist build .nodepyx node_modules",
47
+ "test": "jest --testPathPattern=tests/unit --passWithNoTests",
48
+ "test:integration": "jest --testPathPattern=tests/integration --passWithNoTests",
49
+ "test:all": "jest --passWithNoTests",
50
+ "test:watch": "jest --watch",
51
+ "coverage": "jest --coverage --testPathPattern=tests/unit --passWithNoTests",
52
+ "benchmark": "node benchmarks/run-benchmarks.js",
53
+ "lint": "eslint src --ext .ts,.tsx --max-warnings=0",
54
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
55
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
56
+ "typecheck": "tsc --noEmit -p tsconfig.json",
57
+ "prepublishOnly": "npm run typecheck && npm run lint && npm run test && npm run build",
58
+ "download-prebuilds": "node scripts/download-prebuilds.js",
59
+ "release": "npm run build && npm publish"
60
+ },
61
+ "keywords": [
62
+ "python",
63
+ "nodejs",
64
+ "bridge",
65
+ "pandas",
66
+ "numpy",
67
+ "scikit-learn",
68
+ "pytorch",
69
+ "ml",
70
+ "ai",
71
+ "data-science",
72
+ "cpython",
73
+ "native-addon",
74
+ "n-api",
75
+ "typescript",
76
+ "next.js",
77
+ "edge",
78
+ "proxy",
79
+ "async",
80
+ "in-process"
81
+ ],
82
+ "author": "nodepyx contributors",
83
+ "license": "MIT",
84
+ "repository": {
85
+ "type": "git",
86
+ "url": "https://github.com/nodepyx/nodepyx.git"
87
+ },
88
+ "bugs": {
89
+ "url": "https://github.com/nodepyx/nodepyx/issues"
90
+ },
91
+ "homepage": "https://nodepyx.dev",
92
+ "dependencies": {
93
+ "@msgpack/msgpack": "^3.0.0",
94
+ "node-addon-api": "^8.0.0",
95
+ "node-gyp-build": "^4.8.0"
96
+ },
97
+ "devDependencies": {
98
+ "@types/jest": "^29.5.0",
99
+ "@types/node": "^22.0.0",
100
+ "@types/react": "^19.2.17",
101
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
102
+ "@typescript-eslint/parser": "^7.0.0",
103
+ "eslint": "^8.57.0",
104
+ "jest": "^29.7.0",
105
+ "node-gyp": "^10.1.0",
106
+ "prettier": "^3.3.0",
107
+ "react": "^19.2.7",
108
+ "ts-jest": "^29.2.0",
109
+ "typescript": "^5.9.3"
110
+ },
111
+ "peerDependencies": {
112
+ "next": ">=14.0.0"
113
+ },
114
+ "peerDependenciesMeta": {
115
+ "next": {
116
+ "optional": true
117
+ }
118
+ },
119
+ "engines": {
120
+ "node": ">=18.0.0",
121
+ "npm": ">=9.0.0"
122
+ },
123
+ "os": [
124
+ "linux",
125
+ "darwin",
126
+ "win32"
127
+ ],
128
+ "cpu": [
129
+ "x64",
130
+ "arm64"
131
+ ],
132
+ "binary": {
133
+ "module_name": "nodepyx_addon",
134
+ "module_path": "./prebuilds/{platform}-{arch}",
135
+ "host": "https://github.com/nodepyx/nodepyx/releases/download/"
136
+ },
137
+ "jest": {
138
+ "preset": "ts-jest",
139
+ "testEnvironment": "node",
140
+ "roots": [
141
+ "<rootDir>/tests"
142
+ ],
143
+ "testPathIgnorePatterns": [
144
+ "/node_modules/",
145
+ "/dist/"
146
+ ],
147
+ "moduleNameMapper": {
148
+ "^nodepyx$": "<rootDir>/src/index.ts",
149
+ "^nodepyx/(.*)$": "<rootDir>/src/$1"
150
+ },
151
+ "transform": {
152
+ "^.+\\.tsx?$": [
153
+ "ts-jest",
154
+ {
155
+ "tsconfig": {
156
+ "strict": true,
157
+ "esModuleInterop": true,
158
+ "skipLibCheck": true,
159
+ "moduleResolution": "node"
160
+ }
161
+ }
162
+ ]
163
+ },
164
+ "coverageDirectory": "coverage",
165
+ "collectCoverageFrom": [
166
+ "src/**/*.ts",
167
+ "!src/**/*.d.ts",
168
+ "!src/native/**",
169
+ "!src/nextjs/PyProvider.tsx"
170
+ ],
171
+ "coverageReporters": [
172
+ "text",
173
+ "lcov",
174
+ "html"
175
+ ]
176
+ }
177
+ }
@@ -0,0 +1,433 @@
1
+ """
2
+ error_handler.py
3
+ ================
4
+ Python-side exception handling and traceback formatting for nodepyx.
5
+
6
+ This module is the single place where Python exceptions are converted to
7
+ the structured error dict that ErrorTranslator.ts on the Node.js side
8
+ deserialises into ``PythonError`` instances.
9
+
10
+ Wire format
11
+ ────────────
12
+ The dict returned by every public function in this module follows the shape
13
+ expected by the TypeScript ``PyErrorInfo`` interface:
14
+
15
+ .. code-block:: typescript
16
+
17
+ interface PyErrorInfo {
18
+ type: string; // Python exception class name
19
+ message: string; // str(exc)
20
+ traceback: string; // formatted traceback (human-readable)
21
+ frames: FrameInfo[];
22
+ cause: PyErrorInfo | null;
23
+ context: PyErrorInfo | null;
24
+ notes: string[]; // PEP 678 __notes__ (Python 3.11+)
25
+ pythonVersion: string;
26
+ }
27
+
28
+ interface FrameInfo {
29
+ filename: string;
30
+ lineno: number;
31
+ name: string; // function / method name
32
+ line: string; // source line (if available)
33
+ }
34
+
35
+ Usage
36
+ ─────
37
+ The C++ addon calls ``handle_exception()`` inside every try/catch block that
38
+ wraps Python execution. The result is forwarded as-is to the JavaScript layer.
39
+
40
+ Internal helpers like ``_extract_frames`` are also available to test code and
41
+ to nodepyx_runtime.py which calls ``_make_error_from_exc``.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import sys
47
+ import os
48
+ import traceback
49
+ import linecache
50
+ from typing import Any, Dict, List, Optional, Tuple, Type
51
+
52
+ # ─── Version for correlation in logs ─────────────────────────────────────────
53
+
54
+ ERROR_HANDLER_VERSION = "1.0.0"
55
+
56
+ # ─── Exception type hierarchy ─────────────────────────────────────────────────
57
+ # Maps Python built-in exception names → recommended JS error types.
58
+ # Used by ErrorTranslator.ts to select the right JS Error subclass.
59
+
60
+ EXCEPTION_CATEGORY: Dict[str, str] = {
61
+ # Standard errors
62
+ "TypeError": "TypeError",
63
+ "ValueError": "RangeError",
64
+ "IndexError": "RangeError",
65
+ "KeyError": "ReferenceError",
66
+ "AttributeError": "TypeError",
67
+ "NameError": "ReferenceError",
68
+ "ImportError": "Error",
69
+ "ModuleNotFoundError": "Error",
70
+ "SyntaxError": "SyntaxError",
71
+ "RuntimeError": "Error",
72
+ "NotImplementedError": "Error",
73
+ "StopIteration": "Error",
74
+ "GeneratorExit": "Error",
75
+ "OverflowError": "RangeError",
76
+ "ZeroDivisionError": "RangeError",
77
+ "MemoryError": "Error",
78
+ "RecursionError": "RangeError",
79
+ # IO
80
+ "IOError": "Error",
81
+ "OSError": "Error",
82
+ "FileNotFoundError": "Error",
83
+ "FileExistsError": "Error",
84
+ "PermissionError": "Error",
85
+ "TimeoutError": "Error",
86
+ "ConnectionError": "Error",
87
+ "ConnectionResetError": "Error",
88
+ "ConnectionRefusedError": "Error",
89
+ # Numeric
90
+ "ArithmeticError": "RangeError",
91
+ "FloatingPointError": "RangeError",
92
+ # Pandas
93
+ "KeyError": "ReferenceError",
94
+ "ParserError": "SyntaxError",
95
+ # General
96
+ "AssertionError": "Error",
97
+ "SystemExit": "Error",
98
+ "KeyboardInterrupt": "Error",
99
+ "Exception": "Error",
100
+ "BaseException": "Error",
101
+ }
102
+
103
+
104
+ # ─── Public API ───────────────────────────────────────────────────────────────
105
+
106
+ def handle_exception() -> Dict[str, Any]:
107
+ """
108
+ Capture the current exception (from sys.exc_info()) and return a
109
+ structured error dict.
110
+
111
+ Must be called from inside an ``except`` block.
112
+ """
113
+ exc_type, exc_value, exc_tb = sys.exc_info()
114
+ if exc_value is None:
115
+ # No active exception — shouldn't happen, but guard anyway
116
+ return _make_plain_error("RuntimeError", "handle_exception() called with no active exception")
117
+ return _build_error_dict(exc_type, exc_value, exc_tb)
118
+
119
+
120
+ def format_exception(
121
+ exc: BaseException,
122
+ *,
123
+ include_cause: bool = True,
124
+ include_context: bool = True,
125
+ limit: int = 50,
126
+ ) -> Dict[str, Any]:
127
+ """
128
+ Format an already-captured exception *exc* into the wire dict.
129
+
130
+ Parameters
131
+ ----------
132
+ exc:
133
+ The exception to format.
134
+ include_cause:
135
+ Whether to include the ``__cause__`` chain.
136
+ include_context:
137
+ Whether to include the ``__context__`` chain.
138
+ limit:
139
+ Maximum number of traceback frames to include.
140
+ """
141
+ exc_type = type(exc)
142
+ exc_tb = exc.__traceback__
143
+ result = _build_error_dict(exc_type, exc, exc_tb, limit=limit)
144
+
145
+ if include_cause and exc.__cause__ is not None:
146
+ result["cause"] = format_exception(
147
+ exc.__cause__,
148
+ include_cause=True,
149
+ include_context=False,
150
+ limit=limit,
151
+ )
152
+
153
+ if include_context and exc.__context__ is not None and not exc.__suppress_context__:
154
+ result["context"] = format_exception(
155
+ exc.__context__,
156
+ include_cause=False,
157
+ include_context=True,
158
+ limit=limit,
159
+ )
160
+
161
+ return result
162
+
163
+
164
+ def make_error(
165
+ exc_type: str,
166
+ message: str,
167
+ tb: str = "",
168
+ *,
169
+ frames: Optional[List[Dict[str, Any]]] = None,
170
+ cause: Optional[Dict[str, Any]] = None,
171
+ notes: Optional[List[str]] = None,
172
+ ) -> Dict[str, Any]:
173
+ """
174
+ Construct an error dict from primitive components.
175
+ Used by nodepyx_runtime._make_error().
176
+ """
177
+ return {
178
+ "error": {
179
+ "type": exc_type,
180
+ "message": message,
181
+ "traceback": tb,
182
+ "frames": frames or [],
183
+ "cause": cause,
184
+ "context": None,
185
+ "notes": notes or [],
186
+ "pythonVersion": _python_version(),
187
+ "jsErrorType": EXCEPTION_CATEGORY.get(exc_type, "Error"),
188
+ }
189
+ }
190
+
191
+
192
+ # ─── Traceback formatting ────────────────────────────────────────────────────
193
+
194
+ def extract_frames(
195
+ tb: Optional[Any],
196
+ limit: int = 50,
197
+ skip_nodepyx_internals: bool = True,
198
+ ) -> List[Dict[str, Any]]:
199
+ """
200
+ Convert a traceback object into a list of FrameInfo dicts.
201
+
202
+ Parameters
203
+ ----------
204
+ tb:
205
+ Traceback object from ``sys.exc_info()[2]``.
206
+ limit:
207
+ Maximum number of frames.
208
+ skip_nodepyx_internals:
209
+ If True, omit frames from nodepyx's own Python helpers.
210
+ """
211
+ if tb is None:
212
+ return []
213
+
214
+ frames: List[Dict[str, Any]] = []
215
+ tb_list = traceback.extract_tb(tb, limit=limit)
216
+
217
+ for frame in tb_list:
218
+ filename = frame.filename or "<unknown>"
219
+ name = frame.name or "<unknown>"
220
+ lineno = frame.lineno or 0
221
+ line = (frame.line or "").strip()
222
+
223
+ if skip_nodepyx_internals and _is_nodepyx_internal(filename):
224
+ continue
225
+
226
+ frames.append({
227
+ "filename": os.path.abspath(filename) if filename != "<unknown>" else filename,
228
+ "lineno": lineno,
229
+ "name": name,
230
+ "line": line,
231
+ })
232
+
233
+ return frames
234
+
235
+
236
+ def format_traceback(
237
+ tb: Optional[Any],
238
+ exc_type: Optional[Type[BaseException]] = None,
239
+ exc_value: Optional[BaseException] = None,
240
+ limit: int = 50,
241
+ ) -> str:
242
+ """
243
+ Return a human-readable traceback string (multi-line).
244
+ Mirrors Python's built-in ``traceback.format_exception()``.
245
+ """
246
+ if tb is None and exc_value is not None:
247
+ tb = exc_value.__traceback__
248
+ if tb is None:
249
+ if exc_value is not None:
250
+ return f"{type(exc_value).__name__}: {exc_value}"
251
+ return ""
252
+ lines = traceback.format_exception(exc_type, exc_value, tb, limit=limit)
253
+ return "".join(lines)
254
+
255
+
256
+ # ─── Helpers used by the C++ addon directly ──────────────────────────────────
257
+
258
+ def safe_str(obj: Any) -> str:
259
+ """
260
+ Safely convert *obj* to str, catching any exceptions that __str__ might
261
+ raise (common with misbehaving C extensions).
262
+ """
263
+ try:
264
+ return str(obj)
265
+ except Exception:
266
+ try:
267
+ return repr(obj)
268
+ except Exception:
269
+ return "<unrepresentable object>"
270
+
271
+
272
+ def classify_exception(exc: BaseException) -> Dict[str, str]:
273
+ """
274
+ Return a dict with ``type``, ``module``, ``jsErrorType`` for *exc*.
275
+ Used by the C++ bridge to select behaviour before full serialization.
276
+ """
277
+ cls_name = type(exc).__name__
278
+ module = getattr(type(exc), '__module__', 'builtins') or 'builtins'
279
+ return {
280
+ "type": cls_name,
281
+ "module": module,
282
+ "jsErrorType": EXCEPTION_CATEGORY.get(cls_name, "Error"),
283
+ "isBuiltin": str(module in ('builtins', '__builtin__', 'exceptions')),
284
+ }
285
+
286
+
287
+ def is_retriable(exc: BaseException) -> bool:
288
+ """
289
+ Return True if *exc* is a transient error that the caller may retry.
290
+ Covers network timeouts, resource temporarily unavailable, etc.
291
+ """
292
+ cls_name = type(exc).__name__
293
+ retriable = {
294
+ "TimeoutError", "ConnectionResetError", "BrokenPipeError",
295
+ "ConnectionAbortedError", "OSError",
296
+ }
297
+ if cls_name in retriable:
298
+ return True
299
+ msg = safe_str(exc).lower()
300
+ return any(kw in msg for kw in ("timeout", "timed out", "resource temporarily unavailable"))
301
+
302
+
303
+ # ─── Private helpers ──────────────────────────────────────────────────────────
304
+
305
+ def _build_error_dict(
306
+ exc_type: Optional[Type[BaseException]],
307
+ exc_value: BaseException,
308
+ exc_tb: Optional[Any],
309
+ limit: int = 50,
310
+ ) -> Dict[str, Any]:
311
+ cls_name = exc_type.__name__ if exc_type else type(exc_value).__name__
312
+ message = safe_str(exc_value)
313
+ tb_str = format_traceback(exc_tb, exc_type, exc_value, limit=limit)
314
+ frames = extract_frames(exc_tb, limit=limit)
315
+ notes = _get_notes(exc_value)
316
+
317
+ return {
318
+ "error": {
319
+ "type": cls_name,
320
+ "message": message,
321
+ "traceback": tb_str,
322
+ "frames": frames,
323
+ "cause": None, # populated by format_exception if needed
324
+ "context": None,
325
+ "notes": notes,
326
+ "pythonVersion": _python_version(),
327
+ "jsErrorType": EXCEPTION_CATEGORY.get(cls_name, "Error"),
328
+ }
329
+ }
330
+
331
+
332
+ def _make_plain_error(exc_type: str, message: str) -> Dict[str, Any]:
333
+ return {
334
+ "error": {
335
+ "type": exc_type,
336
+ "message": message,
337
+ "traceback": "",
338
+ "frames": [],
339
+ "cause": None,
340
+ "context": None,
341
+ "notes": [],
342
+ "pythonVersion": _python_version(),
343
+ "jsErrorType": EXCEPTION_CATEGORY.get(exc_type, "Error"),
344
+ }
345
+ }
346
+
347
+
348
+ def _python_version() -> str:
349
+ return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
350
+
351
+
352
+ def _get_notes(exc: BaseException) -> List[str]:
353
+ """Return PEP 678 __notes__ if present (Python 3.11+)."""
354
+ notes = getattr(exc, '__notes__', None)
355
+ if isinstance(notes, list):
356
+ return [safe_str(n) for n in notes]
357
+ return []
358
+
359
+
360
+ def _is_nodepyx_internal(filename: str) -> bool:
361
+ """Return True if *filename* belongs to nodepyx's own Python helpers."""
362
+ if not filename:
363
+ return False
364
+ basename = os.path.basename(filename)
365
+ nodepyx_files = {
366
+ "nodepyx_runtime.py",
367
+ "error_handler.py",
368
+ "serializer.py",
369
+ "type_inspector.py",
370
+ }
371
+ return basename in nodepyx_files
372
+
373
+
374
+ # ─── Context managers ─────────────────────────────────────────────────────────
375
+
376
+ class catch_python_error:
377
+ """
378
+ Context manager that catches any exception and stores it as a wire dict.
379
+
380
+ Usage::
381
+
382
+ handler = catch_python_error()
383
+ with handler:
384
+ risky_operation()
385
+ if handler.error:
386
+ return handler.error # forward to JS
387
+ """
388
+
389
+ def __init__(self) -> None:
390
+ self.error: Optional[Dict[str, Any]] = None
391
+
392
+ def __enter__(self) -> "catch_python_error":
393
+ return self
394
+
395
+ def __exit__(
396
+ self,
397
+ exc_type: Optional[Type[BaseException]],
398
+ exc_value: Optional[BaseException],
399
+ exc_tb: Optional[Any],
400
+ ) -> bool:
401
+ if exc_value is not None:
402
+ self.error = _build_error_dict(exc_type, exc_value, exc_tb)
403
+ return True # suppress exception
404
+ return False
405
+
406
+
407
+ # ─── Structured logging helpers ──────────────────────────────────────────────
408
+
409
+ def log_exception(exc: BaseException, context: str = "") -> None:
410
+ """
411
+ Print a structured exception summary to stderr.
412
+ Used internally when errors cannot be forwarded to JS.
413
+ """
414
+ info = classify_exception(exc)
415
+ prefix = f"[nodepyx] Exception in {context}: " if context else "[nodepyx] Exception: "
416
+ print(f"{prefix}{info['type']}: {safe_str(exc)}", file=sys.stderr)
417
+ if exc.__traceback__:
418
+ tb_lines = traceback.format_tb(exc.__traceback__)
419
+ for line in tb_lines:
420
+ print(line.rstrip(), file=sys.stderr)
421
+
422
+
423
+ # ─── Smoke test (run directly) ────────────────────────────────────────────────
424
+
425
+ if __name__ == "__main__":
426
+ # Quick sanity check
427
+ try:
428
+ 1 / 0
429
+ except ZeroDivisionError:
430
+ err = handle_exception()
431
+ import json
432
+ print(json.dumps(err, indent=2, default=str))
433
+