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
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
|
+
|