├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── binding.gyp ├── index.d.ts ├── index.js ├── index.mjs ├── logo.png ├── package-lock.json ├── package.json ├── scripts ├── pylink.js ├── pysearch.js └── rpaths.js ├── src ├── addon.cpp ├── cpyobject.h ├── pyinterpreter.cpp └── pyinterpreter.h └── test ├── logreg.py ├── nodetest.py ├── nodetestre.py ├── test.js ├── test.mjs ├── testml.js └── worker.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 22.x, 23.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: python3 --version 32 | - run: pip3 install numpy 33 | - run: npm test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | node_modules 4 | test/__pycache__ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | node_modules 4 | test/__pycache__ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Menyhert Hegedus (hmenyus@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![node-calls-python](https://github.com/hmenyus/node-calls-python/blob/main/logo.png) 3 | 4 | # node-calls-python - call Python from Node.js directly in-process without spawning processes 5 | 6 | ## Suitable for running your ML or deep learning models from Node directly 7 | 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/hmenyus) 9 | [![Node.js CI](https://github.com/hmenyus/node-calls-python/actions/workflows/node.js.yml/badge.svg)](https://github.com/hmenyus/node-calls-python/actions/workflows/node.js.yml) 10 | [![npm](https://img.shields.io/npm/v/node-calls-python)](https://www.npmjs.com/package/node-calls-python) 11 | 12 | ## Motivation 13 | Current solutions spawn a new process whenever you want to run Python code in Node.js and communicate via IPC using sockets, stdin/stdout, etc. 14 | But creating new processes every time you want to run Python code could be a major overhead and can lead to significant performance penalties. 15 | If the execution time of your Python code is less than creating a new process, you will see significant performance problems because your Node.js code will keep creating new processes instead of executing your Python code. 16 | Suppose you have a few NumPy calls in Python: do you want to create a new process for that? I guess your answer is no. 17 | In this case, running the Python code in-process is a much better solution because using the embedded Python interpreter is much faster than creating new processes and does not require any IPC to pass the data around. The data can stay in memory and requires only some conversions between Python and Node types (using the N-API and Python C API). 18 | 19 | ## Installation 20 | ``` 21 | npm install node-calls-python 22 | ``` 23 | ## Installation FAQ 24 | Sometimes you have to install prerequisites to make it work. 25 | ### **Linux**: install node, npm, node-gyp, python3, python3-dev, g++ and make 26 | 27 | #### Install Node 28 | ``` 29 | sudo apt install curl 30 | curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash - 31 | sudo apt install nodejs 32 | ``` 33 | 34 | #### Install Python 35 | ``` 36 | sudo apt install python3 37 | sudo apt install python3-dev 38 | ``` 39 | 40 | #### Install Node-gyp 41 | ``` 42 | sudo apt install make 43 | sudo apt install g++ 44 | sudo npm install -g node-gyp 45 | ``` 46 | 47 | ### **Windows**: install [NodeJS](https://nodejs.org/en/download/) and [Python](https://www.python.org/downloads/) 48 | 49 | #### Install Node-gyp if missing 50 | ``` 51 | npm install --global --production windows-build-tools 52 | npm install -g node-gyp 53 | ``` 54 | 55 | ### **Mac**: install XCode from AppStore, [Node.js](https://nodejs.org/en/download/) and [Python](https://www.python.org/downloads/) 56 | ``` 57 | npm install node-calls-python 58 | ``` 59 | 60 | #### If you see installation problems on Mac with ARM (E.g. using M1 Pro), try to specify 'arch' and/or 'target_arch' parameters for npm 61 | ``` 62 | npm install --arch=arm64 --target_arch=arm64 node-calls-python 63 | ``` 64 | 65 | ## Examples 66 | 67 | ### Calling a simple python function 68 | Let's say you have the following python code in **test.py** 69 | ```python 70 | import numpy as np 71 | 72 | def multiple(a, b): 73 | return np.multiply(a, b).tolist() 74 | ``` 75 | 76 | Then to call this function directly you can do this in Node 77 | ```javascript 78 | const nodecallspython = require("node-calls-python"); 79 | 80 | const py = nodecallspython.interpreter; 81 | 82 | py.import("path/to/test.py").then(async function(pymodule) { 83 | const result = await py.call(pymodule, "multiple", [1, 2, 3, 4], [2, 3, 4, 5]); 84 | console.log(result); 85 | }); 86 | ``` 87 | 88 | Or to call this function by using the synchronous version 89 | ```javascript 90 | const nodecallspython = require("node-calls-python"); 91 | 92 | const py = nodecallspython.interpreter; 93 | 94 | py.import("path/to/test.py").then(async function(pymodule) { 95 | const result = py.callSync(pymodule, "multiple", [1, 2, 3, 4], [2, 3, 4, 5]); 96 | console.log(result); 97 | }); 98 | ``` 99 | 100 | ### Creating python objects 101 | Let's say you have the following python code in **test.py** 102 | ```python 103 | import numpy as np 104 | 105 | class Calculator: 106 | vector = [] 107 | 108 | def __init__(self, vector): 109 | self.vector = vector 110 | 111 | def multiply(self, scalar, vector): 112 | return np.add(np.multiply(scalar, self.vector), vector).tolist() 113 | ``` 114 | 115 | Then to instance the class directly in Node 116 | ```javascript 117 | const nodecallspython = require("node-calls-python"); 118 | 119 | const py = nodecallspython.interpreter; 120 | 121 | py.import("path/to/test.py").then(async function(pymodule) { 122 | const pyobj = await py.create(pymodule, "Calculator", [1.4, 5.5, 1.2, 4.4]); 123 | const result = await py.call(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4]); 124 | }); 125 | ``` 126 | 127 | Or to instance the class synchronously and directly in Node 128 | ```javascript 129 | const nodecallspython = require("node-calls-python"); 130 | 131 | const py = nodecallspython.interpreter; 132 | 133 | py.import("path/to/test.py").then(async function(pymodule) { 134 | const pyobj = py.createSync(pymodule, "Calculator", [1.4, 5.5, 1.2, 4.4]); 135 | const result = await py.callSync(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4]); // you can use async version (call) as well 136 | }); 137 | ``` 138 | 139 | ### Running python code 140 | ```javascript 141 | const nodecallspython = require("node-calls-python"); 142 | 143 | const py = nodecallspython.interpreter; 144 | 145 | py.import("path/to/test.py").then(async function(pymodule) { 146 | await py.exec(pymodule, "run_my_code(1, 2, 3)"); // exec will run any python code but the return value is not propagated 147 | const result = await py.eval(pymodule, "run_my_code(1, 2, 3)"); // result will hold the output of run_my_code 148 | console.log(result); 149 | }); 150 | ``` 151 | 152 | Running python code synchronously 153 | ```javascript 154 | const nodecallspython = require("node-calls-python"); 155 | 156 | const py = nodecallspython.interpreter; 157 | 158 | const pymodule = py.importSync("path/to/test.py"); 159 | await py.execSync(pymodule, "run_my_code(1, 2, 3)"); // exec will run any python code but the return value is not propagated 160 | const result = py.evalSync(pymodule, "run_my_code(1, 2, 3)"); // result will hold the output of run_my_code 161 | console.log(result); 162 | ``` 163 | 164 | ### Reimporting a python module 165 | You have to set **allowReimport** parameter to **true** when calling **import/importSync**. 166 | 167 | ```javascript 168 | const nodecallspython = require("node-calls-python"); 169 | 170 | const py = nodecallspython.interpreter; 171 | 172 | let pymodule = py.importSync("path/to/test.py"); 173 | pymodule = py.importSync("path/to/test.py", true); 174 | ``` 175 | 176 | ### Development Mode 177 | During development, you may want to update your python code running inside Node without restarting your Node process. To achieve this you can reimport your python modules. 178 | All your python modules will be reimported where the filename of your python module matches the string parameter: ```path/to/your/python/code```. 179 | ```javascript 180 | const nodecallspython = require("node-calls-python"); 181 | 182 | const py = nodecallspython.interpreter; 183 | 184 | py.reimport('path/to/your/python/code'); 185 | ``` 186 | 187 | Another option is to run ***node-calls-python*** in development mode. In this case, once you have updated your python code under ```path/to/your/python/code``` the runtime will automatically reimport the changed modules. 188 | ```javascript 189 | const nodecallspython = require("node-calls-python"); 190 | 191 | const py = nodecallspython.interpreter; 192 | 193 | py.developmentMode('path/to/your/python/code'); 194 | ``` 195 | 196 | ### Passing kwargs 197 | Javascript has no similar concept to kwargs of Python. Therefore a little hack is needed here. If you pass an object with **__kwargs** property set to **true** as a parameter to **call/callSync/create/createSync** the object will be mapped to kwargs. 198 | 199 | ```javascript 200 | const nodecallspython = require("node-calls-python"); 201 | 202 | const py = nodecallspython.interpreter; 203 | 204 | let pymodule = py.importSync("path/to/test.py"); 205 | py.callSync(pymodule, "your_function", arg1, arg2, {"name1": value1, "name2": value2, "__kwargs": true }) 206 | ``` 207 | 208 | ```python 209 | def your_function(arg1, arg2, **kwargs): 210 | print(kwargs) 211 | ``` 212 | 213 | ### Passing JavaScript functions to Python 214 | If you want to trigger a call from your Python code back to JavaScript this feature could be useful. 215 | 216 | ```javascript 217 | const nodecallspython = require("node-calls-python"); 218 | 219 | const py = nodecallspython.interpreter; 220 | 221 | let pymodule = py.importSync("path/to/test.py"); 222 | 223 | function jsFunction(arg1, arg2, arg3) 224 | { 225 | console.log(arg1, arg2, arg3); 226 | return arg3 + 1; 227 | } 228 | 229 | py.callSync(pymodule, "your_function", arg1, arg2, jsFunction); 230 | ``` 231 | 232 | ```python 233 | def your_function(arg1, arg2, jsFunction): 234 | jsResult = jsFunction(arg1 + arg2, "any string", 42); 235 | print(jsResult); 236 | ``` 237 | 238 | You can also do this using the async API. 239 | ```javascript 240 | py.call(pymodule, "your_function", arg1, arg2, jsFunction); 241 | ``` 242 | 243 | By default, the async Python call will wait for the execution of the JavaScript function by synchronizing the libuv thread (used by the Python call) and the main thread (used by the JavaScript function). 244 | So the order of execution will look like this: 245 | ``` 246 | - start of py.call 247 | - start of your_function 248 | - start of jsFunction 249 | - end of jsFunction 250 | - end of your_function 251 | - end of py.call 252 | ``` 253 | 254 | If you do not want to synchronize the execution of your JavaScript and Python code, you have to turn this off by calling **setSyncJsAndPyInCallback(false)** on the interpreter. 255 | ```javascript 256 | py.setSyncJsAndPyInCallback(false); 257 | ``` 258 | 259 | In this case, one possible order of the execution could look like this (the actual order is determined by the runtime. jsFunction will run completely async). 260 | ``` 261 | - start of py.call 262 | - start of your_function 263 | - put jsFunction to the queue of the runtime 264 | - end of your_function 265 | - end of py.call 266 | - start of jsFunction 267 | - end of jsFunction 268 | ``` 269 | 270 | Because jsFunction runs async, it is not possible to pass the result of jsFunction back to Python. But passing arguments from Python to jsFunction is still possible. 271 | 272 | ### Working with Python multiprocessing 273 | Python uses sys.executable variable when creating new processes. Because the interpreter is embedded into Node, sys.executable points to the Node executable. ***node-calls-python*** automatically overrides this setting in the multiprocessing module to point to the real Python executable. In case it does not work or you want to use a different Python executable, call ***setPythonExecutable(absolute-path-to-your-python-executable)*** before using the multiprocessing module. 274 | ```javascript 275 | py.setPythonExecutable(absolute-path-to-your-python-executable); 276 | ``` 277 | 278 | 279 | ### Doing some ML with Python and Node 280 | Let's say you have the following python code in **logreg.py** 281 | ```python 282 | from sklearn.datasets import load_iris, load_digits 283 | from sklearn.linear_model import LogisticRegression 284 | 285 | class LogReg: 286 | logreg = None 287 | 288 | def __init__(self, dataset): 289 | if (dataset == "iris"): 290 | X, y = load_iris(return_X_y=True) 291 | else: 292 | X, y = load_digits(return_X_y=True) 293 | 294 | self.logreg = LogisticRegression(random_state=42, solver='lbfgs', multi_class='multinomial') 295 | self.logreg.fit(X, y) 296 | 297 | def predict(self, X): 298 | return self.logreg.predict_proba(X).tolist() 299 | ``` 300 | 301 | Then you can do this in Node 302 | ```javascript 303 | const nodecallspython = require("node-calls-python"); 304 | 305 | const py = nodecallspython.interpreter; 306 | 307 | py.import("logreg.py")).then(async function(pymodule) { // import the python module 308 | const logreg = await py.create(pymodule, "LogReg", "iris"); // create the instance of the classifier 309 | 310 | const predict = await py.call(logreg, "predict", [[1.4, 5.5, 1.2, 4.4]]); // call predict 311 | console.log(predict); 312 | }); 313 | ``` 314 | ### Using as ES Module 315 | You can import ***node-calls-python*** as an ***ES module***. 316 | 317 | ```javascript 318 | import { interpreter as py } from 'node-calls-python'; 319 | 320 | let pymodule = py.importSync(pyfile); 321 | ``` 322 | 323 | ### Using in Next.js 324 | If you see the following error when importing in Next.js 325 | ```Module not found: Can't resolve './build/Release/nodecallspython'``` 326 | 327 | You have to add the following code to your next.config.mjs because currently Next.js cannot bundle native node addons properly. 328 | For more details, please see [serverComponentsExternalPackages in Next.js](https://nextjs.org/docs/app/api-reference/next-config-js/serverComponentsExternalPackages) 329 | 330 | ``` 331 | /** @type {import('next').NextConfig} */ 332 | const nextConfig = { 333 | experimental: { 334 | serverComponentsExternalPackages: [ 335 | 'node-calls-python' 336 | ] 337 | } 338 | }; 339 | 340 | export default nextConfig; 341 | ``` 342 | 343 | ### Using Python venv 344 | You have to add the proper import path so that python could use your installed packages from your venv. 345 | 346 | If you have created a venv by ```python -m venv your-venv``` your installed python packages can be found under ```your-venv/Lib/site-packages```. 347 | So you have to use ```addImportPath``` before importing any module to pick up the python packages from your venv. 348 | 349 | ```javascript 350 | const nodecallspython = require("node-calls-python"); 351 | 352 | const py = nodecallspython.interpreter; 353 | 354 | py.addImportPath(your-venv/Lib/site-packages) 355 | ``` 356 | 357 | ### Working Around Linking Errors on Linux 358 | If you get an error like this while trying to call Python code 359 | ```ImportError: /usr/local/lib/python3.7/dist-packages/cpython-37m-arm-linux-gnueabihf.so: undefined symbol: PyExc_RuntimeError``` 360 | 361 | You can fix it by passing the name of your libpython shared library to fixlink 362 | ```javascript 363 | const nodecallspython = require("node-calls-python"); 364 | 365 | const py = nodecallspython.interpreter; 366 | py.fixlink('libpython3.7m.so'); 367 | ``` 368 | 369 | ### [See more examples here](https://github.com/hmenyus/node-calls-python/tree/main/test) 370 | 371 | ## Supported data mapping 372 | 373 | ### From Node to Python 374 | ``` 375 | - undefined to None 376 | - null to None 377 | - boolean to boolean 378 | - number to double or long (as appropriate) 379 | - int32 to long 380 | - uint32 to long 381 | - int64 to long 382 | - string to unicode (string) 383 | - array to list 384 | - object to dictionary 385 | - ArrayBuffer to bytes 386 | - Buffer to bytes 387 | - TypedArray to bytes 388 | - Function to function 389 | ``` 390 | 391 | ### From Python to Node 392 | ``` 393 | - None to undefined 394 | - boolean to boolean 395 | - double to number 396 | - long to int64 397 | - unicode (string) to string 398 | - list to array 399 | - tuple to array 400 | - set to array 401 | - dictionary to object 402 | - numpy.array to array (this has limited support, will convert everything to number or string) 403 | - bytes to ArrayBuffer 404 | - bytearray to ArrayBuffer 405 | ``` 406 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "nodecallspython", 5 | "conditions": [ 6 | ['OS=="win"', { 7 | "include_dirs" : [ 8 | " Promise; 16 | importSync: (filename: string, allowReimport: boolean) => PyModule; 17 | 18 | create: (module: PyModule, className: string, ...args: any[]) => Promise; 19 | createSync: (module: PyModule, className: string, ...args: any[]) => PyObject; 20 | 21 | call: (module: PyModule | PyObject, functionName: string, ...args: any[]) => Promise; 22 | callSync: (module: PyModule | PyObject, functionName: string, ...args: any[]) => unknown; 23 | 24 | exec: (module: PyModule | PyObject, codeToRun: string) => Promise; 25 | execSync: (module: PyModule | PyObject, codeToRun: string) => unknown; 26 | 27 | eval: (module: PyModule | PyObject, codeToRun: string) => Promise; 28 | evalSync: (module: PyModule | PyObject, codeToRun: string) => unknown; 29 | 30 | fixlink: (fileName: string) => void; 31 | 32 | reimport: (directory: string) => void; 33 | 34 | addImportPath: (path: string) => void; 35 | 36 | developmentMode: (paths: string[]) => void; 37 | 38 | setSyncJsAndPyInCallback: (syncJsAndPy: boolean) => void; 39 | 40 | setPythonExecutable: (executable: string) => void; 41 | } 42 | 43 | export const interpreter: Interpreter; 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const nodecallspython = require("./build/Release/nodecallspython"); 5 | const chokidar = require("chokidar"); 6 | 7 | class Interpreter 8 | { 9 | loadPython(dir) 10 | { 11 | const debug = process.env.NODECALLSPYTHON_DEBUG !== undefined; 12 | if (debug) 13 | console.log("Loading python from " + dir); 14 | 15 | let found = false; 16 | if (fs.existsSync(dir)) 17 | { 18 | fs.readdirSync(dir).forEach(file => { 19 | if (file.match(/libpython3.*\.so/)) 20 | { 21 | try 22 | { 23 | const filename = path.join(dir, file); 24 | if (debug) 25 | console.log("Running fixlink on " + filename); 26 | 27 | this.fixlink(filename); 28 | found = true; 29 | } 30 | catch(e) 31 | { 32 | console.error(e); 33 | } 34 | } 35 | }); 36 | } 37 | 38 | if (!found && debug) 39 | console.log("Not found"); 40 | 41 | return found; 42 | } 43 | 44 | getPythonExecutableImpl(command) 45 | { 46 | const stdout = execSync(command); 47 | if (stdout) 48 | { 49 | const result = stdout.toString().trim().split("\n"); 50 | if (result.length > 0) 51 | return result[0].trim(); 52 | } 53 | return undefined; 54 | } 55 | 56 | getPythonExecutable() 57 | { 58 | if (process.platform === "win32") 59 | return this.getPythonExecutableImpl("where python"); 60 | else 61 | return this.getPythonExecutableImpl("which python3"); 62 | } 63 | 64 | constructor() 65 | { 66 | this.py = new nodecallspython.PyInterpreter(); 67 | if (process.platform === "linux") 68 | { 69 | const stdout = execSync("python3-config --configdir"); 70 | let found = false; 71 | if (stdout) 72 | { 73 | const dir = stdout.toString().trim(); 74 | const res = this.loadPython(dir); 75 | if (res) 76 | found = true; 77 | } 78 | 79 | if (!found) 80 | { 81 | const stdout = execSync("python3-config --ldflags"); 82 | if (stdout) 83 | { 84 | const split = stdout.toString().trim().split(" "); 85 | split.forEach(s => { 86 | if (s.startsWith("-L")) 87 | this.loadPython(s.substring(2)); 88 | }); 89 | } 90 | } 91 | } 92 | 93 | try 94 | { 95 | this.setPythonExecutable(this.getPythonExecutable()); 96 | } 97 | catch(e) 98 | { 99 | } 100 | } 101 | 102 | import(filename, allowReimport = false) 103 | { 104 | return new Promise(function(resolve, reject) { 105 | try 106 | { 107 | this.py.import(filename, allowReimport, function(handler, error) { 108 | if (handler) 109 | resolve(handler); 110 | else 111 | reject(error); 112 | }); 113 | } 114 | catch(e) 115 | { 116 | reject(e); 117 | } 118 | }.bind(this)); 119 | } 120 | 121 | importSync(filename, allowReimport = false) 122 | { 123 | return this.py.importSync(filename, allowReimport); 124 | } 125 | 126 | call(handler, func, ...args) 127 | { 128 | return new Promise(function(resolve, reject) { 129 | try 130 | { 131 | this.py.call(handler, func, ...args, function(result, error) { 132 | if (error) 133 | reject(error); 134 | else 135 | resolve(result); 136 | }); 137 | } 138 | catch(e) 139 | { 140 | reject(e); 141 | } 142 | }.bind(this)); 143 | } 144 | 145 | callSync(handler, func, ...args) 146 | { 147 | return this.py.callSync(handler, func, ...args); 148 | } 149 | 150 | create(handler, func, ...args) 151 | { 152 | return new Promise(function(resolve, reject) { 153 | try 154 | { 155 | this.py.create(handler, func, ...args, function(result, error) { 156 | if (error) 157 | reject(error); 158 | else 159 | resolve(result); 160 | }); 161 | } 162 | catch(e) 163 | { 164 | reject(e); 165 | } 166 | }.bind(this)); 167 | } 168 | 169 | createSync(handler, func, ...args) 170 | { 171 | return this.py.createSync(handler, func, ...args); 172 | } 173 | 174 | fixlink(filename) 175 | { 176 | return this.py.fixlink(filename); 177 | } 178 | 179 | reimport(directory) 180 | { 181 | return this.py.reimport(directory); 182 | } 183 | 184 | exec(handler, code) 185 | { 186 | return new Promise(function(resolve, reject) { 187 | try 188 | { 189 | this.py.exec(handler, code, function(result, error) { 190 | if (error) 191 | reject(error); 192 | else 193 | resolve(result); 194 | }); 195 | } 196 | catch(e) 197 | { 198 | reject(e); 199 | } 200 | }.bind(this)); 201 | } 202 | 203 | execSync(handler, code) 204 | { 205 | return this.py.execSync(handler, code); 206 | } 207 | 208 | eval(handler, code) 209 | { 210 | return new Promise(function(resolve, reject) { 211 | try 212 | { 213 | this.py.eval(handler, code, function(result, error) { 214 | if (error) 215 | reject(error); 216 | else 217 | resolve(result); 218 | }); 219 | } 220 | catch(e) 221 | { 222 | reject(e); 223 | } 224 | }.bind(this)); 225 | } 226 | 227 | evalSync(handler, code) 228 | { 229 | return this.py.evalSync(handler, code); 230 | } 231 | 232 | addImportPath(path) 233 | { 234 | return this.py.addImportPath(path); 235 | } 236 | 237 | setSyncJsAndPyInCallback(syncJsAndPy) 238 | { 239 | return this.py.setSyncJsAndPyInCallback(syncJsAndPy); 240 | } 241 | 242 | setPythonExecutable(executable) 243 | { 244 | const escaped = executable.trim().replace(/\\/g, '\\\\\\\\'); 245 | this.py.execSync({}, 'import multiprocessing; multiprocessing.set_executable(\'' + escaped + '\');'); 246 | } 247 | 248 | developmentMode(paths) 249 | { 250 | const watcher = chokidar.watch(paths, { 251 | persistent: true, 252 | ignoreInitial: true, 253 | }); 254 | watcher.on("change", (fileName) => { 255 | const ext = path.extname(fileName); 256 | if (ext == ".py" || ext == "py") 257 | this.reimport(fileName); 258 | }); 259 | } 260 | } 261 | 262 | let py = new Interpreter(); 263 | 264 | module.exports = { 265 | interpreter: py 266 | } 267 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | import cjs from './index.js'; 2 | 3 | export const interpreter = cjs.interpreter; 4 | 5 | export default cjs.interpreter; 6 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmenyus/node-calls-python/79cca1ec28a49c96a3b4a86496ed5052edd8fde1/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-calls-python", 3 | "version": "1.11.1", 4 | "license": "MIT", 5 | "description": "This module lets you run python code inside node without spawning new processes", 6 | "authors": [ 7 | "Menyhert Hegedus (hmenyus@gmail.com)" 8 | ], 9 | "keywords": [ 10 | "python", 11 | "c++", 12 | "v8", 13 | "node", 14 | "nodejs", 15 | "node-js", 16 | "napi" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/hmenyus/node-calls-python.git" 21 | }, 22 | "licenses": [ 23 | { 24 | "type": "MIT", 25 | "url": "https://github.com/hmenyus/node-calls-python/blob/main/LICENSE" 26 | } 27 | ], 28 | "files": [ 29 | "binding.gyp", 30 | "index.d.ts", 31 | "index.js", 32 | "index.mjs", 33 | "src/", 34 | "scripts/" 35 | ], 36 | "main": "index.js", 37 | "scripts": { 38 | "build": "npm install .", 39 | "test": "jest" 40 | }, 41 | "exports": { 42 | "types": "./index.d.ts", 43 | "require": "./index.js", 44 | "import": "./index.mjs" 45 | }, 46 | "dependencies": { 47 | "chokidar": "^3.6.0" 48 | }, 49 | "devDependencies": { 50 | "jest": "^27.5.1", 51 | "node-gyp": "^10.1.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/pylink.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const path = require('path'); 3 | 4 | let stdout; 5 | try 6 | { 7 | stdout = execSync("python3-config --ldflags --embed"); 8 | } 9 | catch(e) 10 | { 11 | stdout = execSync("python3-config --ldflags"); 12 | } 13 | 14 | let linkerLine = stdout.toString().trim(); 15 | 16 | // conda hack starts here 17 | try 18 | { 19 | const condaBase = execSync("conda info --base 2>&1"); 20 | if (condaBase) 21 | { 22 | const condaBaseString = condaBase.toString().trim(); 23 | if (linkerLine.includes(condaBaseString)) 24 | linkerLine += " -L" + path.join(condaBaseString, "lib") 25 | } 26 | } 27 | catch(e) 28 | { 29 | } 30 | 31 | console.log(linkerLine); 32 | -------------------------------------------------------------------------------- /scripts/pysearch.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require('fs') 3 | 4 | let pathenv = process.env.PATH.split(";"); 5 | let arg = process.argv[2]; 6 | 7 | for (let p of pathenv) 8 | { 9 | if (p.toLowerCase().includes("python")) 10 | { 11 | if (fs.existsSync(path.join(p, "python.exe")) || fs.existsSync(path.join(p, "python"))) 12 | { 13 | if (arg == "lib") 14 | { 15 | fs.readdir(path.join(p, "libs"), function(err, items) { 16 | let length = 0; 17 | let result = ""; 18 | for (let i of items) 19 | { 20 | if (i.startsWith("python")) 21 | { 22 | if (i.endsWith(".lib") || i.endsWith(".a")) 23 | { 24 | if (length < i.length) 25 | { 26 | length = i.length; 27 | result = i; 28 | } 29 | } 30 | } 31 | } 32 | 33 | let index = result.lastIndexOf("."); 34 | if (index != -1) 35 | result = result.substring(0, index); 36 | 37 | console.log("-l" + result); 38 | }); 39 | } 40 | else 41 | console.log(path.join(p, arg)); 42 | 43 | return; 44 | } 45 | } 46 | } 47 | 48 | console.log("Cannot find PYTHON"); 49 | -------------------------------------------------------------------------------- /scripts/rpaths.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const path = require('path'); 3 | 4 | let stdout; 5 | try 6 | { 7 | stdout = execSync("python3-config --ldflags --embed"); 8 | } 9 | catch(e) 10 | { 11 | stdout = execSync("python3-config --ldflags"); 12 | } 13 | 14 | if (stdout) 15 | { 16 | const splits = stdout.toString().trim().split(" "); 17 | 18 | // conda hack starts here 19 | try 20 | { 21 | const condaBase = execSync("conda info --base 2>&1"); 22 | if (condaBase) 23 | splits.push("-L" + path.join(condaBase.toString().trim(), "lib")); 24 | } 25 | catch(e) 26 | { 27 | } 28 | 29 | const result = []; 30 | splits.forEach(s => { 31 | if (s.startsWith("-L")) 32 | result.push("-Wl,-rpath," + s.substring(2)); 33 | }); 34 | 35 | console.log(" " + result.join(" ")); 36 | } 37 | -------------------------------------------------------------------------------- /src/addon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #ifndef WIN32 7 | #include 8 | #endif 9 | #include "cpyobject.h" 10 | #include "pyinterpreter.h" 11 | 12 | #define DECLARE_NAPI_METHOD(name, func) { name, 0, func, 0, 0, 0, napi_default, 0 } 13 | #define CHECK(func) { if (func != napi_ok) { napi_throw_error(env, "error", #func); return; } } 14 | #define CHECKNULL(func) { if (func != napi_ok) { napi_throw_error(env, "error", #func); return nullptr; } } 15 | #define CHECKNONE(func) { if (func != napi_ok) { napi_throw_error(env, "error", #func); return {}; } } 16 | 17 | namespace nodecallspython 18 | { 19 | struct BaseTask 20 | { 21 | napi_async_work m_work; 22 | napi_ref m_callback; 23 | PyInterpreter* m_py; 24 | napi_env m_env; 25 | std::string m_error; 26 | 27 | ~BaseTask() 28 | { 29 | napi_delete_reference(m_env, m_callback); 30 | napi_delete_async_work(m_env, m_work); 31 | } 32 | }; 33 | 34 | struct ImportTask : public BaseTask 35 | { 36 | std::string m_name; 37 | std::string m_handler; 38 | bool m_allowReimport; 39 | }; 40 | 41 | struct CallTask : public BaseTask 42 | { 43 | std::string m_handler; 44 | std::string m_func; 45 | bool m_isFunc; 46 | 47 | CPyObject m_args; 48 | CPyObject m_kwargs; 49 | CPyObject m_result; 50 | 51 | ~CallTask() 52 | { 53 | GIL gil; 54 | m_args = CPyObject(); 55 | m_kwargs = CPyObject(); 56 | m_result = CPyObject(); 57 | } 58 | }; 59 | 60 | struct ExecTask : public BaseTask 61 | { 62 | std::string m_handler; 63 | std::string m_code; 64 | bool m_eval; 65 | 66 | CPyObject m_result; 67 | 68 | ~ExecTask() 69 | { 70 | GIL gil; 71 | m_result = CPyObject(); 72 | } 73 | }; 74 | 75 | class Handler 76 | { 77 | PyInterpreter* m_py; 78 | std::string m_handler; 79 | 80 | public: 81 | Handler(PyInterpreter* py, const std::string& handler) : m_py(py), m_handler(handler) {} 82 | 83 | ~Handler() 84 | { 85 | GIL gil; 86 | m_py->release(m_handler); 87 | } 88 | 89 | static void Destructor(napi_env env, void* nativeObject, void* finalize_hint) 90 | { 91 | delete reinterpret_cast(nativeObject); 92 | } 93 | }; 94 | 95 | napi_value createHandler(napi_env env, PyInterpreter* py, const std::string& stringhandler) 96 | { 97 | napi_value key; 98 | CHECKNULL(napi_create_string_utf8(env, "handler", NAPI_AUTO_LENGTH, &key)); 99 | 100 | napi_value handler; 101 | CHECKNULL(napi_create_string_utf8(env, stringhandler.c_str(), NAPI_AUTO_LENGTH, &handler)); 102 | 103 | napi_value result; 104 | CHECKNULL(napi_create_object(env, &result)); 105 | 106 | CHECKNULL(napi_set_property(env, result, key, handler)); 107 | 108 | CHECKNULL(napi_add_finalizer(env, result, new Handler(py, stringhandler), Handler::Destructor, nullptr, nullptr)); 109 | 110 | return result; 111 | } 112 | 113 | template 114 | napi_value createHandler(napi_env env, T* task) 115 | { 116 | return createHandler(env, task->m_py, task->m_handler); 117 | } 118 | 119 | static void CallAsync(napi_env env, void* data) 120 | { 121 | auto task = static_cast(data); 122 | GIL gil; 123 | try 124 | { 125 | if (task->m_isFunc) 126 | task->m_result = task->m_py->call(task->m_handler, task->m_func, task->m_args, task->m_kwargs); 127 | else 128 | task->m_handler = task->m_py->create(task->m_handler, task->m_func, task->m_args, task->m_kwargs); 129 | } 130 | catch(const std::exception& e) 131 | { 132 | task->m_error = e.what(); 133 | } 134 | } 135 | 136 | static void ExecAsync(napi_env env, void* data) 137 | { 138 | auto task = static_cast(data); 139 | GIL gil; 140 | try 141 | { 142 | task->m_result = task->m_py->exec(task->m_handler, task->m_code, task->m_eval); 143 | } 144 | catch(const std::exception& e) 145 | { 146 | task->m_error = e.what(); 147 | } 148 | } 149 | 150 | static void ImportAsync(napi_env env, void* data) 151 | { 152 | auto task = static_cast(data); 153 | GIL gil; 154 | try 155 | { 156 | task->m_handler = task->m_py->import(task->m_name, task->m_allowReimport); 157 | } catch(const std::exception& e) 158 | { 159 | task->m_error = e.what(); 160 | } 161 | } 162 | 163 | void handleError(napi_env env, const BaseTask& task) 164 | { 165 | napi_value undefined; 166 | CHECK(napi_get_undefined(env, &undefined)); 167 | 168 | napi_value error; 169 | const std::string unknownError("Unknown python error"); 170 | CHECK(napi_create_string_utf8(env, task.m_error.empty() ? unknownError.c_str() : task.m_error.c_str(), NAPI_AUTO_LENGTH, &error)); 171 | 172 | napi_value args[] = {undefined, error}; 173 | 174 | napi_value callback; 175 | CHECK(napi_get_reference_value(env, task.m_callback, &callback)); 176 | 177 | napi_value global; 178 | CHECK(napi_get_global(env, &global)); 179 | 180 | napi_value result; 181 | CHECK(napi_call_function(env, global, callback, 2, args, &result)); 182 | } 183 | 184 | static void ImportComplete(napi_env env, napi_status status, void* data) 185 | { 186 | std::unique_ptr task(static_cast(data)); 187 | task->m_env = env; 188 | 189 | if (!task->m_error.empty()) 190 | handleError(env, *task); 191 | else 192 | { 193 | napi_value global; 194 | CHECK(napi_get_global(env, &global)); 195 | 196 | auto handler = createHandler(env, task.get()); 197 | 198 | napi_value callback; 199 | CHECK(napi_get_reference_value(env, task->m_callback, &callback)); 200 | 201 | napi_value result; 202 | CHECK(napi_call_function(env, global, callback, 1, &handler, &result)); 203 | } 204 | } 205 | 206 | static void CallComplete(napi_env env, napi_status status, void* data) 207 | { 208 | std::unique_ptr task(static_cast(data)); 209 | task->m_env = env; 210 | 211 | if (!task->m_error.empty()) 212 | handleError(env, *task); 213 | else 214 | { 215 | napi_value global; 216 | CHECK(napi_get_global(env, &global)); 217 | 218 | napi_value args; 219 | if (task->m_isFunc) 220 | { 221 | GIL gil; 222 | args = task->m_py->convert(env, *task->m_result); 223 | } 224 | else 225 | { 226 | args = createHandler(env, task.get()); 227 | } 228 | 229 | napi_value callback; 230 | CHECK(napi_get_reference_value(env, task->m_callback, &callback)); 231 | 232 | napi_value result; 233 | CHECK(napi_call_function(env, global, callback, 1, &args, &result)); 234 | } 235 | } 236 | 237 | static void ExecComplete(napi_env env, napi_status status, void* data) 238 | { 239 | std::unique_ptr task(static_cast(data)); 240 | task->m_env = env; 241 | 242 | if (!task->m_error.empty()) 243 | handleError(env, *task); 244 | else 245 | { 246 | napi_value global; 247 | CHECK(napi_get_global(env, &global)); 248 | 249 | napi_value args; 250 | GIL gil; 251 | args = task->m_py->convert(env, *task->m_result); 252 | 253 | napi_value callback; 254 | CHECK(napi_get_reference_value(env, task->m_callback, &callback)); 255 | 256 | napi_value result; 257 | CHECK(napi_call_function(env, global, callback, 1, &args, &result)); 258 | } 259 | } 260 | 261 | class Python 262 | { 263 | napi_env m_env; 264 | napi_ref m_wrapper; 265 | static napi_ref constructor; 266 | 267 | public: 268 | static napi_value Init(napi_env env, napi_value exports) 269 | { 270 | napi_property_descriptor properties[] = 271 | { 272 | DECLARE_NAPI_METHOD("import", import), 273 | DECLARE_NAPI_METHOD("importSync", importSync), 274 | DECLARE_NAPI_METHOD("call", call), 275 | DECLARE_NAPI_METHOD("callSync", callSync), 276 | DECLARE_NAPI_METHOD("create", newClass), 277 | DECLARE_NAPI_METHOD("createSync", newClassSync), 278 | DECLARE_NAPI_METHOD("fixlink", fixlink), 279 | DECLARE_NAPI_METHOD("exec", exec), 280 | DECLARE_NAPI_METHOD("execSync", execSync), 281 | DECLARE_NAPI_METHOD("eval", eval), 282 | DECLARE_NAPI_METHOD("evalSync", evalSync), 283 | DECLARE_NAPI_METHOD("addImportPath", addImportPath), 284 | DECLARE_NAPI_METHOD("reimport", reimport), 285 | DECLARE_NAPI_METHOD("setSyncJsAndPyInCallback", setSyncJsAndPyInCallback) 286 | }; 287 | 288 | napi_value cons; 289 | CHECKNULL(napi_define_class(env, "PyInterpreter", NAPI_AUTO_LENGTH, create, nullptr, 14, properties, &cons)); 290 | 291 | CHECKNULL(napi_create_reference(env, cons, 1, &constructor)); 292 | 293 | CHECKNULL(napi_set_named_property(env, exports, "PyInterpreter", cons)); 294 | 295 | return exports; 296 | } 297 | 298 | static void Destructor(napi_env env, void* nativeObject, void* finalize_hint) 299 | { 300 | delete reinterpret_cast(nativeObject); 301 | } 302 | 303 | static napi_value callImpl(napi_env env, napi_callback_info info, bool isFunc, bool sync) 304 | { 305 | try 306 | { 307 | napi_value jsthis; 308 | size_t argc = 100; 309 | napi_value args[100]; 310 | CHECKNULL(napi_get_cb_info(env, info, &argc, &args[0], &jsthis, nullptr)); 311 | 312 | if (argc < 2) 313 | { 314 | napi_throw_error(env, "args", "Wrong number of arguments"); 315 | return nullptr; 316 | } 317 | 318 | Python* obj; 319 | CHECKNULL(napi_unwrap(env, jsthis, reinterpret_cast(&obj))); 320 | 321 | napi_valuetype handlerT; 322 | CHECKNULL(napi_typeof(env, args[0], &handlerT)); 323 | 324 | napi_valuetype funcT; 325 | CHECKNULL(napi_typeof(env, args[1], &funcT)); 326 | 327 | if (handlerT == napi_object && funcT == napi_string) 328 | { 329 | napi_value key; 330 | CHECKNULL(napi_create_string_utf8(env, "handler", NAPI_AUTO_LENGTH, &key)); 331 | 332 | napi_value value; 333 | CHECKNULL(napi_get_property(env, args[0], key, &value)); 334 | 335 | auto handler = convertString(env, value); 336 | auto func = convertString(env, args[1]); 337 | 338 | std::vector napiargs; 339 | napiargs.reserve(argc - 2); 340 | for (auto i=2u;igetInterpreter(); 347 | auto pyArgs = py.convert(env, napiargs, true); 348 | 349 | napi_value result; 350 | if (isFunc) 351 | { 352 | auto pyres = py.call(handler, func, pyArgs.first, pyArgs.second); 353 | if (pyres) 354 | result = py.convert(env, *pyres); 355 | else 356 | CHECKNULL(napi_get_undefined(env, &result)); 357 | } 358 | else 359 | { 360 | auto newhandler = py.create(handler, func, pyArgs.first, pyArgs.second); 361 | result = createHandler(env, &py, newhandler); 362 | } 363 | 364 | return result; 365 | } 366 | else 367 | { 368 | napi_valuetype callbackT; 369 | CHECKNULL(napi_typeof(env, args[argc - 1], &callbackT)); 370 | 371 | if (callbackT == napi_function) 372 | { 373 | CallTask* task = new CallTask; 374 | task->m_py = &(obj->getInterpreter()); 375 | 376 | task->m_handler = convertString(env, value); 377 | task->m_func = convertString(env, args[1]); 378 | task->m_isFunc = isFunc; 379 | 380 | napi_value optname; 381 | napi_create_string_utf8(env, "Python::call", NAPI_AUTO_LENGTH, &optname); 382 | 383 | { 384 | GIL gil; 385 | std::tie(task->m_args, task->m_kwargs) = obj->getInterpreter().convert(env, napiargs, false); 386 | } 387 | 388 | CHECKNULL(napi_create_reference(env, args[argc - 1], 1, &task->m_callback)); 389 | 390 | CHECKNULL(napi_create_async_work(env, args[1], optname, CallAsync, CallComplete, task, &task->m_work)); 391 | CHECKNULL(napi_queue_async_work(env, task->m_work)); 392 | } 393 | } 394 | } 395 | else 396 | { 397 | napi_throw_error(env, "args", "Wrong type of arguments"); 398 | } 399 | } 400 | catch(const std::exception& e) 401 | { 402 | napi_throw_error(env, "py", e.what()); 403 | } 404 | 405 | return nullptr; 406 | } 407 | 408 | static napi_value execImpl(napi_env env, napi_callback_info info, bool eval, bool sync) 409 | { 410 | try 411 | { 412 | napi_value jsthis; 413 | size_t argc = 3; 414 | napi_value args[3]; 415 | CHECKNULL(napi_get_cb_info(env, info, &argc, &args[0], &jsthis, nullptr)); 416 | 417 | if (argc < 2) 418 | { 419 | napi_throw_error(env, "args", "Wrong number of arguments"); 420 | return nullptr; 421 | } 422 | 423 | Python* obj; 424 | CHECKNULL(napi_unwrap(env, jsthis, reinterpret_cast(&obj))); 425 | 426 | napi_valuetype handlerT; 427 | CHECKNULL(napi_typeof(env, args[0], &handlerT)); 428 | 429 | napi_valuetype codeToExecT; 430 | CHECKNULL(napi_typeof(env, args[1], &codeToExecT)); 431 | 432 | if (handlerT == napi_object && codeToExecT == napi_string) 433 | { 434 | napi_value key; 435 | CHECKNULL(napi_create_string_utf8(env, "handler", NAPI_AUTO_LENGTH, &key)); 436 | 437 | napi_value value; 438 | CHECKNULL(napi_get_property(env, args[0], key, &value)); 439 | 440 | if (sync) 441 | { 442 | GIL gil; 443 | auto& py = obj->getInterpreter(); 444 | auto pyres = py.exec(convertString(env, value), convertString(env, args[1]), eval); 445 | napi_value result; 446 | if (pyres) 447 | result = py.convert(env, *pyres); 448 | else 449 | CHECKNULL(napi_get_undefined(env, &result)); 450 | return result; 451 | } 452 | else 453 | { 454 | napi_valuetype callbackT; 455 | CHECKNULL(napi_typeof(env, args[argc - 1], &callbackT)); 456 | 457 | if (callbackT == napi_function) 458 | { 459 | ExecTask* task = new ExecTask; 460 | task->m_py = &(obj->getInterpreter()); 461 | 462 | task->m_handler = convertString(env, value); 463 | task->m_code = convertString(env, args[1]); 464 | task->m_eval = eval; 465 | 466 | napi_value optname; 467 | napi_create_string_utf8(env, "Python::exec", NAPI_AUTO_LENGTH, &optname); 468 | 469 | CHECKNULL(napi_create_reference(env, args[argc - 1], 1, &task->m_callback)); 470 | 471 | CHECKNULL(napi_create_async_work(env, args[1], optname, ExecAsync, ExecComplete, task, &task->m_work)); 472 | CHECKNULL(napi_queue_async_work(env, task->m_work)); 473 | } 474 | } 475 | } 476 | else 477 | { 478 | napi_throw_error(env, "args", "Wrong type of arguments"); 479 | } 480 | } 481 | catch(const std::exception& e) 482 | { 483 | napi_throw_error(env, "py", e.what()); 484 | } 485 | 486 | return nullptr; 487 | } 488 | 489 | static napi_value importImpl(napi_env env, napi_callback_info info, bool sync) 490 | { 491 | try 492 | { 493 | napi_value jsthis; 494 | size_t argc = 3; 495 | napi_value args[3]; 496 | CHECKNULL(napi_get_cb_info(env, info, &argc, &args[0], &jsthis, nullptr)); 497 | 498 | if (argc != 2 && argc != 3) 499 | { 500 | napi_throw_error(env, "args", "Wrong number of arguments"); 501 | return nullptr; 502 | } 503 | 504 | Python* obj; 505 | CHECKNULL(napi_unwrap(env, jsthis, reinterpret_cast(&obj))); 506 | 507 | napi_valuetype moduleT; 508 | CHECKNULL(napi_typeof(env, args[0], &moduleT)); 509 | 510 | napi_valuetype allowReimportT; 511 | CHECKNULL(napi_typeof(env, args[1], &allowReimportT)); 512 | 513 | if (moduleT == napi_string && allowReimportT == napi_boolean) 514 | { 515 | auto allowReimport = false; 516 | CHECKNULL(napi_get_value_bool(env, args[1], &allowReimport)); 517 | 518 | if (sync) 519 | { 520 | GIL gil; 521 | auto name = convertString(env, args[0]); 522 | auto& py = obj->getInterpreter(); 523 | 524 | auto handler = py.import(name, allowReimport); 525 | return createHandler(env, &py, handler); 526 | } 527 | else 528 | { 529 | napi_valuetype callbackT; 530 | CHECKNULL(napi_typeof(env, args[2], &callbackT)); 531 | 532 | if (callbackT == napi_function) 533 | { 534 | ImportTask* task = new ImportTask; 535 | task->m_py = &(obj->getInterpreter()); 536 | task->m_name = convertString(env, args[0]); 537 | task->m_allowReimport = allowReimport; 538 | 539 | napi_value optname; 540 | napi_create_string_utf8(env, "Python::import", NAPI_AUTO_LENGTH, &optname); 541 | 542 | CHECKNULL(napi_create_reference(env, args[2], 1, &task->m_callback)); 543 | 544 | CHECKNULL(napi_create_async_work(env, args[2], optname, ImportAsync, ImportComplete, task, &task->m_work)); 545 | CHECKNULL(napi_queue_async_work(env, task->m_work)); 546 | } 547 | } 548 | } 549 | else 550 | { 551 | napi_throw_error(env, "args", "Wrong type of arguments"); 552 | } 553 | } 554 | catch(const std::exception& e) 555 | { 556 | napi_throw_error(env, "py", e.what()); 557 | } 558 | 559 | return nullptr; 560 | } 561 | 562 | private: 563 | std::unique_ptr m_py; 564 | 565 | Python(napi_env env) : m_env(env), m_wrapper(nullptr) 566 | { 567 | m_py = std::make_unique(); 568 | } 569 | 570 | ~Python() 571 | { 572 | napi_delete_reference(m_env, m_wrapper); 573 | } 574 | 575 | PyInterpreter& getInterpreter() { return *m_py; } 576 | 577 | static napi_value create(napi_env env, napi_callback_info info) 578 | { 579 | napi_value target; 580 | CHECKNULL(napi_get_new_target(env, info, &target)); 581 | auto isConstructor = target != nullptr; 582 | 583 | if (isConstructor) 584 | { 585 | napi_value jsthis; 586 | CHECKNULL(napi_get_cb_info(env, info, nullptr, 0, &jsthis, nullptr)); 587 | 588 | Python* obj = new Python(env); 589 | 590 | CHECKNULL(napi_wrap(env, jsthis, reinterpret_cast(obj), Python::Destructor, nullptr, &obj->m_wrapper)); 591 | 592 | return jsthis; 593 | } 594 | else 595 | { 596 | napi_value jsthis; 597 | CHECKNULL(napi_get_cb_info(env, info, nullptr, 0, &jsthis, nullptr)); 598 | 599 | napi_value cons; 600 | CHECKNULL(napi_get_reference_value(env, constructor, &cons)); 601 | 602 | napi_value instance; 603 | CHECKNULL(napi_new_instance(env, cons, 0, nullptr, &instance)); 604 | 605 | return instance; 606 | } 607 | } 608 | 609 | static std::string convertString(napi_env env, napi_value value) 610 | { 611 | size_t length = 0; 612 | napi_get_value_string_utf8(env, value, NULL, 0, &length); 613 | std::string result(length, ' '); 614 | napi_get_value_string_utf8(env, value, &result[0], length + 1, &length); 615 | return result; 616 | } 617 | 618 | static napi_value import(napi_env env, napi_callback_info info) 619 | { 620 | return importImpl(env, info, false); 621 | } 622 | 623 | static napi_value importSync(napi_env env, napi_callback_info info) 624 | { 625 | return importImpl(env, info, true); 626 | } 627 | 628 | static napi_value call(napi_env env, napi_callback_info info) 629 | { 630 | return callImpl(env, info, true, false); 631 | } 632 | 633 | static napi_value callSync(napi_env env, napi_callback_info info) 634 | { 635 | return callImpl(env, info, true, true); 636 | } 637 | 638 | static napi_value exec(napi_env env, napi_callback_info info) 639 | { 640 | return execImpl(env, info, false, false); 641 | } 642 | 643 | static napi_value execSync(napi_env env, napi_callback_info info) 644 | { 645 | return execImpl(env, info, false, true); 646 | } 647 | 648 | static napi_value eval(napi_env env, napi_callback_info info) 649 | { 650 | return execImpl(env, info, true, false); 651 | } 652 | 653 | static napi_value evalSync(napi_env env, napi_callback_info info) 654 | { 655 | return execImpl(env, info, true, true); 656 | } 657 | 658 | static napi_value newClass(napi_env env, napi_callback_info info) 659 | { 660 | return callImpl(env, info, false, false); 661 | } 662 | 663 | static napi_value newClassSync(napi_env env, napi_callback_info info) 664 | { 665 | return callImpl(env, info, false, true); 666 | } 667 | 668 | static std::pair getStringArgument(napi_env env, napi_callback_info info) 669 | { 670 | napi_value jsthis; 671 | size_t argc = 1; 672 | napi_value args[1]; 673 | CHECKNONE(napi_get_cb_info(env, info, &argc, &args[0], &jsthis, nullptr)); 674 | 675 | if (argc != 1) 676 | { 677 | napi_throw_error(env, "args", "Must have 1 arguments"); 678 | return {}; 679 | } 680 | 681 | Python* obj; 682 | CHECKNONE(napi_unwrap(env, jsthis, reinterpret_cast(&obj))); 683 | 684 | napi_valuetype valuetype; 685 | CHECKNONE(napi_typeof(env, args[0], &valuetype)); 686 | 687 | if (valuetype == napi_string) 688 | return { convertString(env, args[0]), obj }; 689 | else 690 | { 691 | napi_throw_error(env, "args", "Wrong type of arguments"); 692 | } 693 | 694 | return {}; 695 | } 696 | 697 | static napi_value fixlink(napi_env env, napi_callback_info info) 698 | { 699 | #ifdef WIN32 700 | return nullptr; 701 | #else 702 | std::string filename; 703 | Python* obj = nullptr; 704 | std::tie(filename, obj) = getStringArgument(env, info); 705 | if (filename.empty() || !obj) 706 | return nullptr; 707 | 708 | auto result = dlopen(filename.c_str(), RTLD_LAZY | RTLD_GLOBAL); 709 | if (!result) 710 | { 711 | auto error = dlerror(); 712 | if (error) 713 | napi_throw_error(env, "args", error); 714 | else 715 | napi_throw_error(env, "args", "Unknown error of dlopen"); 716 | } 717 | 718 | return nullptr; 719 | #endif 720 | } 721 | 722 | static napi_value reimport(napi_env env, napi_callback_info info) 723 | { 724 | std::string directory; 725 | Python* obj = nullptr; 726 | std::tie(directory, obj) = getStringArgument(env, info); 727 | if (directory.empty() || !obj) 728 | return nullptr; 729 | 730 | auto& py = obj->getInterpreter(); 731 | try 732 | { 733 | GIL gil; 734 | py.reimport(directory); 735 | } 736 | catch(const std::exception& e) 737 | { 738 | napi_throw_error(env, "py", e.what()); 739 | } 740 | 741 | return nullptr; 742 | } 743 | 744 | static napi_value addImportPath(napi_env env, napi_callback_info info) 745 | { 746 | std::string path; 747 | Python* obj = nullptr; 748 | std::tie(path, obj) = getStringArgument(env, info); 749 | if (path.empty() || !obj) 750 | return nullptr; 751 | 752 | auto& py = obj->getInterpreter(); 753 | try 754 | { 755 | GIL gil; 756 | py.addImportPath(path); 757 | } 758 | catch(const std::exception& e) 759 | { 760 | napi_throw_error(env, "py", e.what()); 761 | } 762 | 763 | return nullptr; 764 | } 765 | 766 | static napi_value setSyncJsAndPyInCallback(napi_env env, napi_callback_info info) 767 | { 768 | napi_value jsthis; 769 | size_t argc = 1; 770 | napi_value args[1]; 771 | CHECKNULL(napi_get_cb_info(env, info, &argc, &args[0], &jsthis, nullptr)); 772 | 773 | if (argc != 1) 774 | { 775 | napi_throw_error(env, "args", "Wrong number of arguments"); 776 | return nullptr; 777 | } 778 | 779 | Python* obj; 780 | CHECKNULL(napi_unwrap(env, jsthis, reinterpret_cast(&obj))); 781 | 782 | napi_valuetype syncJsAndPyT; 783 | CHECKNULL(napi_typeof(env, args[0], &syncJsAndPyT)); 784 | 785 | if (syncJsAndPyT == napi_boolean) 786 | { 787 | auto syncJsAndPy = false; 788 | CHECKNULL(napi_get_value_bool(env, args[0], &syncJsAndPy)); 789 | 790 | obj->getInterpreter().setSyncJsAndPyInCallback(syncJsAndPy); 791 | } 792 | else 793 | { 794 | napi_throw_error(env, "args", "Wrong type of arguments"); 795 | } 796 | 797 | return nullptr; 798 | } 799 | }; 800 | 801 | napi_ref Python::constructor; 802 | } 803 | 804 | NAPI_MODULE_INIT() 805 | { 806 | return nodecallspython::Python::Init(env, exports); 807 | } 808 | -------------------------------------------------------------------------------- /src/cpyobject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace nodecallspython 5 | { 6 | class CPyObject 7 | { 8 | private: 9 | PyObject* m_py; 10 | public: 11 | CPyObject() : m_py(nullptr) {} 12 | 13 | CPyObject(PyObject* py) : m_py(py) {} 14 | 15 | ~CPyObject() 16 | { 17 | if (m_py) 18 | Py_DECREF(m_py); 19 | } 20 | 21 | PyObject* operator*() 22 | { 23 | return m_py; 24 | } 25 | 26 | operator bool() 27 | { 28 | return m_py ? true : false; 29 | } 30 | 31 | CPyObject(const CPyObject& other) 32 | { 33 | m_py = other.m_py; 34 | 35 | if (m_py) 36 | Py_INCREF(m_py); 37 | } 38 | 39 | void operator=(const CPyObject& other) 40 | { 41 | if (m_py != other.m_py) 42 | { 43 | if (m_py) 44 | Py_DECREF(m_py); 45 | 46 | m_py = other.m_py; 47 | 48 | if (m_py) 49 | Py_INCREF(m_py); 50 | } 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/pyinterpreter.cpp: -------------------------------------------------------------------------------- 1 | #include "pyinterpreter.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace nodecallspython; 9 | 10 | bool nodecallspython::PyInterpreter::m_inited = false; 11 | std::mutex nodecallspython::PyInterpreter::m_mutex; 12 | 13 | namespace 14 | { 15 | void signal_handler_int(int) 16 | { 17 | std::exit(130); 18 | } 19 | } 20 | 21 | PyInterpreter::PyInterpreter() : m_state(nullptr), m_syncJsAndPy(true) 22 | { 23 | std::lock_guard l(m_mutex); 24 | 25 | if (!m_inited) 26 | { 27 | Py_InitializeEx(0); 28 | 29 | #if PY_MINOR_VERSION < 9 30 | if (!PyEval_ThreadsInitialized()) 31 | PyEval_InitThreads(); 32 | #endif 33 | 34 | Py_DECREF(PyImport_ImportModule("threading")); 35 | 36 | m_state = PyEval_SaveThread(); 37 | 38 | if (!std::getenv("NODE_CALLS_PYTHON_IGNORE_SIGINT")) 39 | PyOS_setsig(SIGINT, ::signal_handler_int); 40 | 41 | m_inited = true; 42 | } 43 | } 44 | 45 | PyInterpreter::~PyInterpreter() 46 | { 47 | { 48 | GIL gil; 49 | m_objs = {}; 50 | } 51 | 52 | if (m_state) 53 | { 54 | PyEval_RestoreThread(m_state); 55 | Py_Finalize(); 56 | } 57 | } 58 | 59 | #define CHECK(func) { auto res = func; if (res != napi_ok) { throw std::runtime_error(std::string(#func) + " returned with an error: " + std::to_string(static_cast(func))); } } 60 | 61 | namespace 62 | { 63 | napi_value convert(napi_env env, PyObject* obj); 64 | 65 | napi_value fillArray(napi_env env, CPyObject& iterator, napi_value array) 66 | { 67 | PyObject *item; 68 | auto i = 0; 69 | while ((item = PyIter_Next(*iterator))) 70 | { 71 | CHECK(napi_set_element(env, array, i, ::convert(env, item))); 72 | Py_DECREF(item); 73 | ++i; 74 | } 75 | 76 | return array; 77 | } 78 | 79 | napi_value createArrayBuffer(napi_env env, size_t size, const char* ptr) 80 | { 81 | void* data = nullptr; 82 | napi_value buffer; 83 | CHECK(napi_create_arraybuffer(env, size, &data, &buffer)); 84 | if (size && ptr) 85 | memcpy(data, ptr, size); 86 | return buffer; 87 | } 88 | 89 | napi_value convert(napi_env env, PyObject* obj) 90 | { 91 | if (PyBool_Check(obj)) 92 | { 93 | napi_value result; 94 | CHECK(napi_get_boolean(env, obj == Py_True, &result)); 95 | return result; 96 | } 97 | else if (PyUnicode_Check(obj)) 98 | { 99 | Py_ssize_t size; 100 | auto str = PyUnicode_AsUTF8AndSize(obj, &size); 101 | 102 | napi_value result; 103 | CHECK(napi_create_string_utf8(env, str, size, &result)); 104 | return result; 105 | } 106 | else if (PyLong_Check(obj)) 107 | { 108 | napi_value result; 109 | CHECK(napi_create_int64(env, PyLong_AsLong(obj), &result)); 110 | return result; 111 | } 112 | else if (PyFloat_Check(obj)) 113 | { 114 | napi_value result; 115 | CHECK(napi_create_double(env, PyFloat_AsDouble(obj), &result)); 116 | return result; 117 | } 118 | else if (PyList_Check(obj)) 119 | { 120 | auto length = PyList_Size(obj); 121 | napi_value array; 122 | CHECK(napi_create_array_with_length(env, length, &array)); 123 | 124 | for (auto i = 0u; i < length; ++i) 125 | CHECK(napi_set_element(env, array, i, convert(env, PyList_GetItem(obj, i)))); 126 | 127 | return array; 128 | } 129 | else if (PyTuple_Check(obj)) 130 | { 131 | auto length = PyTuple_Size(obj); 132 | napi_value array; 133 | CHECK(napi_create_array_with_length(env, length, &array)); 134 | 135 | for (auto i = 0u; i < length; ++i) 136 | CHECK(napi_set_element(env, array, i, convert(env, PyTuple_GetItem(obj, i)))); 137 | 138 | return array; 139 | } 140 | else if (PySet_Check(obj)) 141 | { 142 | auto length = PySet_Size(obj); 143 | napi_value array; 144 | CHECK(napi_create_array_with_length(env, length, &array)); 145 | 146 | CPyObject iterator = PyObject_GetIter(obj); 147 | 148 | return fillArray(env, iterator, array); 149 | } 150 | else if (PyBytes_Check(obj)) 151 | { 152 | auto size = PyBytes_Size(obj); 153 | auto ptr = PyBytes_AsString(obj); 154 | 155 | return createArrayBuffer(env, size, ptr); 156 | } 157 | else if (PyByteArray_Check(obj)) 158 | { 159 | auto size = PyByteArray_Size(obj); 160 | auto ptr = PyByteArray_AsString(obj); 161 | 162 | return createArrayBuffer(env, size, ptr); 163 | } 164 | else if (PyDict_Check(obj)) 165 | { 166 | napi_value object; 167 | CHECK(napi_create_object(env, &object)); 168 | 169 | PyObject *key, *value; 170 | Py_ssize_t pos = 0; 171 | 172 | while (PyDict_Next(obj, &pos, &key, &value)) 173 | CHECK(napi_set_property(env, object, convert(env, key), convert(env, value))); 174 | 175 | return object; 176 | } 177 | else if (obj == Py_None) 178 | { 179 | napi_value undefined; 180 | CHECK(napi_get_undefined(env, &undefined)); 181 | return undefined; 182 | } 183 | else 184 | { 185 | CPyObject iterator = PyObject_GetIter(obj); 186 | if (iterator) 187 | { 188 | napi_value array; 189 | CHECK(napi_create_array(env, &array)); 190 | 191 | return fillArray(env, iterator, array); 192 | } 193 | else 194 | { 195 | // attempt to force convert to support numpy arrays 196 | PyErr_Clear(); 197 | 198 | // cannot decide between int and double if we do not know the type here, so cast everything to double 199 | auto value = PyFloat_AsDouble(obj); 200 | if (!PyErr_Occurred()) 201 | { 202 | napi_value result; 203 | CHECK(napi_create_double(env, value, &result)); 204 | return result; 205 | } 206 | else 207 | { 208 | PyErr_Clear(); 209 | Py_ssize_t size; 210 | auto str = PyUnicode_AsUTF8AndSize(obj, &size); 211 | if (str) 212 | { 213 | napi_value result; 214 | CHECK(napi_create_string_utf8(env, str, size, &result)); 215 | return result; 216 | } 217 | PyErr_Clear(); 218 | } 219 | 220 | napi_value undefined; 221 | CHECK(napi_get_undefined(env, &undefined)); 222 | return undefined; 223 | } 224 | } 225 | } 226 | 227 | std::vector convertParams(napi_env env, void* data) 228 | { 229 | std::vector params; 230 | auto obj = reinterpret_cast(data); 231 | auto length = PyTuple_Size(obj); 232 | params.reserve(length); 233 | for (auto i = 0u; i < length; ++i) 234 | params.push_back(convert(env, PyTuple_GetItem(obj, i))); 235 | return params; 236 | } 237 | 238 | napi_value callJsImpl(napi_env env, napi_value func, const std::vector& params) 239 | { 240 | napi_value undefined, result; 241 | napi_get_undefined(env, &undefined); 242 | napi_call_function(env, undefined, func, params.size(), params.data(), &result); 243 | return result; 244 | } 245 | 246 | std::pair convert(napi_env env, napi_value arg, bool isSync, bool allowFunc, bool syncJsAndPy); 247 | 248 | void callJs(napi_env env, napi_value func, void* context, void* data) 249 | { 250 | GIL gil; 251 | try 252 | { 253 | CPyObject args(reinterpret_cast(data)); 254 | auto params = convertParams(env, *args); 255 | 256 | callJsImpl(env, func, params); 257 | } 258 | catch(std::exception&) 259 | { 260 | } 261 | } 262 | 263 | PyObject* __callback_function_napi_async(PyObject *self, PyObject* args) 264 | { 265 | auto func = reinterpret_cast(PyCapsule_GetPointer(self, nullptr)); 266 | Py_INCREF(args); 267 | napi_call_threadsafe_function(*func, args, napi_tsfn_nonblocking); 268 | Py_RETURN_NONE; 269 | } 270 | 271 | struct Promise 272 | { 273 | std::promise promise; 274 | PyObject* args; 275 | 276 | Promise(PyObject* args) : args(args) 277 | { 278 | } 279 | }; 280 | 281 | void callJsPromise(napi_env env, napi_value func, void* context, void* data) 282 | { 283 | auto promise = reinterpret_cast(data); 284 | try 285 | { 286 | std::vector params; 287 | { 288 | GIL gil; 289 | CPyObject args(promise->args); 290 | params = convertParams(env, *args); 291 | } 292 | 293 | auto result = callJsImpl(env, func, params); 294 | 295 | { 296 | GIL gil; 297 | auto pyResult = convert(env, result, true, false, false).first; 298 | promise->promise.set_value(pyResult); 299 | } 300 | } 301 | catch(std::exception&) 302 | { 303 | promise->promise.set_value(nullptr); 304 | } 305 | } 306 | 307 | PyObject* __callback_function_napi_async_promise(PyObject *self, PyObject* args) 308 | { 309 | auto func = reinterpret_cast(PyCapsule_GetPointer(self, nullptr)); 310 | Py_INCREF(args); 311 | auto promise = std::make_unique(args); 312 | auto future = promise->promise.get_future(); 313 | 314 | Py_BEGIN_ALLOW_THREADS; 315 | napi_call_threadsafe_function(*func, promise.get(), napi_tsfn_nonblocking); 316 | future.wait(); 317 | Py_END_ALLOW_THREADS; 318 | 319 | return future.get(); 320 | } 321 | 322 | struct SycnCallback 323 | { 324 | napi_env env; 325 | napi_value func; 326 | }; 327 | 328 | PyObject* __callback_function_napi_sync(PyObject *self, PyObject* args) 329 | { 330 | auto func = reinterpret_cast(PyCapsule_GetPointer(self, nullptr)); 331 | auto params = convertParams(func->env, args); 332 | auto result = callJsImpl(func->env, func->func, params); 333 | return convert(func->env, result, true, false, false).first; 334 | } 335 | 336 | void capsuleDestructor(PyObject* obj) 337 | { 338 | auto func = reinterpret_cast(PyCapsule_GetPointer(obj, nullptr)); 339 | napi_release_threadsafe_function(*func, napi_tsfn_abort); 340 | delete func; 341 | } 342 | 343 | void capsuleDestructorSync(PyObject* obj) 344 | { 345 | delete reinterpret_cast(PyCapsule_GetPointer(obj, nullptr)); 346 | } 347 | 348 | PyMethodDef mlAsync = { "__callback_function_napi_async", (PyCFunction)(void(*)(void))__callback_function_napi_async, METH_VARARGS, nullptr }; 349 | PyMethodDef mlAsyncPromise = { "__callback_function_napi_async_promise", (PyCFunction)(void(*)(void))__callback_function_napi_async_promise, METH_VARARGS, nullptr }; 350 | PyMethodDef mlSync = { "__callback_function_napi_sync", (PyCFunction)(void(*)(void))__callback_function_napi_sync, METH_VARARGS, nullptr }; 351 | 352 | PyObject* handleInteger(napi_env env, napi_value arg) 353 | { 354 | //handle integers 355 | int32_t i = 0; 356 | auto res = napi_get_value_int32(env, arg, &i); 357 | if (res != napi_ok) 358 | { 359 | uint32_t i = 0; 360 | res = napi_get_value_uint32(env, arg, &i); 361 | if (res != napi_ok) 362 | { 363 | int64_t i = 0; 364 | res = napi_get_value_int64(env, arg, &i); 365 | if (res == napi_ok) 366 | return PyLong_FromLong(i); 367 | else 368 | throw std::runtime_error("Invalid parameter: unknown type"); 369 | } 370 | else 371 | return PyLong_FromLong(i); 372 | } 373 | else 374 | return PyLong_FromLong(i); 375 | } 376 | 377 | std::pair convert(napi_env env, napi_value arg, bool isSync, bool allowFunc, bool syncJsAndPy) 378 | { 379 | napi_valuetype type; 380 | CHECK(napi_typeof(env, arg, &type)); 381 | 382 | bool isarray = false; 383 | CHECK(napi_is_array(env, arg, &isarray)); 384 | 385 | if (isarray) 386 | { 387 | uint32_t length = 0; 388 | CHECK(napi_get_array_length(env, arg, &length)); 389 | 390 | auto* list = PyList_New(length); 391 | 392 | for (auto i = 0u; i < length; ++i) 393 | { 394 | napi_value value; 395 | CHECK(napi_get_element(env, arg, i, &value)); 396 | PyList_SetItem(list, i, ::convert(env, value, isSync, allowFunc, syncJsAndPy).first); 397 | } 398 | 399 | return { list, false }; 400 | } 401 | 402 | bool isarraybuffer = false; 403 | CHECK(napi_is_arraybuffer(env, arg, &isarraybuffer)); 404 | if (isarraybuffer) 405 | { 406 | void* data = nullptr; 407 | size_t len = 0; 408 | CHECK(napi_get_arraybuffer_info(env, arg, &data, &len)); 409 | auto* bytes = PyBytes_FromStringAndSize((const char*)data, len); 410 | return { bytes, false }; 411 | } 412 | 413 | bool isbuffer = false; 414 | CHECK(napi_is_buffer(env, arg, &isbuffer)); 415 | if (isbuffer) 416 | { 417 | void* data = nullptr; 418 | size_t len = 0; 419 | CHECK(napi_get_buffer_info(env, arg, &data, &len)); 420 | auto* bytes = PyBytes_FromStringAndSize((const char*)data, len); 421 | return { bytes, false }; 422 | } 423 | 424 | bool istypedarray = false; 425 | CHECK(napi_is_typedarray(env, arg, &istypedarray)); 426 | if (istypedarray) 427 | { 428 | napi_typedarray_type type; 429 | void* data = nullptr; 430 | size_t len = 0; 431 | CHECK(napi_get_typedarray_info(env, arg, &type, &len, &data, nullptr, nullptr)); 432 | 433 | switch (type) 434 | { 435 | case napi_int16_array: 436 | case napi_uint16_array: 437 | len *= 2; 438 | break; 439 | case napi_int32_array: 440 | case napi_uint32_array: 441 | case napi_float32_array: 442 | len *= 4; 443 | break; 444 | case napi_float64_array: 445 | case napi_bigint64_array: 446 | case napi_biguint64_array: 447 | len *= 8; 448 | break; 449 | default: 450 | break; 451 | } 452 | 453 | auto* bytes = PyBytes_FromStringAndSize((const char*)data, len); 454 | return { bytes, false }; 455 | } 456 | 457 | bool isdataview = false; 458 | CHECK(napi_is_dataview(env, arg, &isdataview)); 459 | if (isdataview) 460 | { 461 | void* data = nullptr; 462 | size_t len = 0; 463 | CHECK(napi_get_dataview_info(env, arg, &len, &data, nullptr, nullptr)); 464 | auto* bytes = PyBytes_FromStringAndSize((const char*)data, len); 465 | return { bytes, false }; 466 | } 467 | else if (type == napi_undefined || type == napi_null) 468 | { 469 | Py_INCREF(Py_None); 470 | return { Py_None, false }; 471 | } 472 | else if (type == napi_string) 473 | { 474 | size_t length = 0; 475 | CHECK(napi_get_value_string_utf8(env, arg, NULL, 0, &length)); 476 | std::string s(length, ' '); 477 | CHECK(napi_get_value_string_utf8(env, arg, &s[0], length + 1, &length)); 478 | 479 | return { PyUnicode_FromString(s.c_str()), false }; 480 | } 481 | else if (type == napi_number) 482 | { 483 | double d = 0.0; 484 | CHECK(napi_get_value_double(env, arg, &d)); 485 | if ((double) ((int) d) == d) 486 | return { handleInteger(env, arg), false }; 487 | else 488 | return { PyFloat_FromDouble(d), false }; 489 | } 490 | else if (type == napi_boolean) 491 | { 492 | bool b = false; 493 | CHECK(napi_get_value_bool(env, arg, &b)); 494 | return { b ? PyBool_FromLong(1) : PyBool_FromLong(0), false }; 495 | } 496 | else if (type == napi_object) 497 | { 498 | napi_value properties; 499 | CHECK(napi_get_property_names(env, arg, &properties)); 500 | 501 | uint32_t length = 0; 502 | CHECK(napi_get_array_length(env, properties, &length)); 503 | 504 | auto kwargs = false; 505 | 506 | auto* dict = PyDict_New(); 507 | for (auto i = 0u; i < length; ++i) 508 | { 509 | napi_value key; 510 | CHECK(napi_get_element(env, properties, i, &key)); 511 | 512 | napi_value value; 513 | CHECK(napi_get_property(env, arg, key, &value)); 514 | 515 | napi_valuetype keyType; 516 | CHECK(napi_typeof(env, key, &keyType)); 517 | auto thisKwargs = false; 518 | if (keyType == napi_string) 519 | { 520 | napi_valuetype valueType; 521 | CHECK(napi_typeof(env, value, &valueType)); 522 | if (valueType == napi_boolean) 523 | { 524 | size_t length = 0; 525 | CHECK(napi_get_value_string_utf8(env, key, NULL, 0, &length)); 526 | std::string s(length, ' '); 527 | CHECK(napi_get_value_string_utf8(env, key, &s[0], length + 1, &length)); 528 | if (s == "__kwargs") 529 | { 530 | CHECK(napi_get_value_bool(env, value, &thisKwargs)); 531 | } 532 | } 533 | } 534 | 535 | if (thisKwargs) 536 | kwargs = true; 537 | else 538 | { 539 | CPyObject pykey = ::convert(env, key, isSync, allowFunc, syncJsAndPy).first; 540 | 541 | CPyObject pyvalue = ::convert(env, value, isSync, allowFunc, syncJsAndPy).first; 542 | 543 | PyDict_SetItem(dict, *pykey, *pyvalue); 544 | } 545 | } 546 | 547 | return { dict, kwargs }; 548 | } 549 | else if (type == napi_function && allowFunc) 550 | { 551 | if (isSync) 552 | { 553 | CPyObject capsule = PyCapsule_New(new SycnCallback{env, arg}, nullptr, capsuleDestructorSync); 554 | auto function = PyCFunction_New(&mlSync, *capsule); 555 | return { function, false }; 556 | } 557 | else 558 | { 559 | auto tsfn = new napi_threadsafe_function; 560 | 561 | napi_value workName; 562 | CHECK(napi_create_string_utf8(env, "ThreadSafeCallback", NAPI_AUTO_LENGTH, &workName)); 563 | 564 | CPyObject capsule = PyCapsule_New(tsfn, nullptr, capsuleDestructor); 565 | PyObject* function = nullptr; 566 | if (syncJsAndPy) 567 | { 568 | CHECK(napi_create_threadsafe_function(env, arg, nullptr, workName, 0, 1, nullptr, nullptr, nullptr, callJsPromise, tsfn)); 569 | function = PyCFunction_New(&mlAsyncPromise, *capsule); 570 | } 571 | else 572 | { 573 | CHECK(napi_create_threadsafe_function(env, arg, nullptr, workName, 0, 1, nullptr, nullptr, nullptr, callJs, tsfn)); 574 | function = PyCFunction_New(&mlAsync, *capsule); 575 | } 576 | 577 | return { function, false }; 578 | } 579 | } 580 | else 581 | return { handleInteger(env, arg), false }; 582 | 583 | throw std::runtime_error("Invalid parameter: unknown type"); 584 | }} 585 | 586 | std::pair PyInterpreter::convert(napi_env env, const std::vector& args, bool isSync) 587 | { 588 | std::vector paramsVect; 589 | CPyObject kwargs; 590 | paramsVect.reserve(args.size()); 591 | for (auto i=0u;i 2 && name[len - 3] == '.' && name[len - 2] == 'p' && name[len - 1] == 'y') 700 | name = name.substr(0, len - 3); 701 | 702 | PyErr_Clear(); 703 | CPyObject pyModule = PyImport_ImportModule(name.c_str()); 704 | if (!pyModule) 705 | { 706 | handleException(); 707 | return {}; 708 | } 709 | 710 | auto it = m_imports.find(*pyModule); 711 | if (it != m_imports.end() && allowReimport) 712 | { 713 | pyModule = PyImport_ReloadModule(*pyModule); 714 | if (!pyModule) 715 | { 716 | handleException(); 717 | return {}; 718 | } 719 | } 720 | 721 | auto uuid = getUUID(true, pyModule); 722 | m_objs[uuid] = pyModule; 723 | m_imports[*pyModule] = uuid; 724 | return uuid; 725 | } 726 | 727 | CPyObject PyInterpreter::call(const std::string& handler, const std::string& func, CPyObject& args, CPyObject& kwargs) 728 | { 729 | auto it = m_objs.find(handler); 730 | 731 | if(it == m_objs.end()) 732 | throw std::runtime_error("Cannot find handler: " + handler); 733 | 734 | PyErr_Clear(); 735 | CPyObject pyFunc = PyObject_GetAttrString(*(it->second), func.c_str()); 736 | if (pyFunc && PyCallable_Check(*pyFunc)) 737 | { 738 | CPyObject pyResult = PyObject_Call(*pyFunc, *args, *kwargs); 739 | if (!*pyResult) 740 | { 741 | handleException(); 742 | throw std::runtime_error("Unknown python error"); 743 | } 744 | 745 | return pyResult; 746 | } 747 | else 748 | handleException(); 749 | 750 | throw std::runtime_error("Unknown python error"); 751 | } 752 | 753 | CPyObject PyInterpreter::exec(const std::string& handler, const std::string& code, bool eval) 754 | { 755 | auto globals = CPyObject{PyDict_New()}; 756 | 757 | PyObject* localsPtr; 758 | if (handler.empty()) 759 | localsPtr = *globals; 760 | else 761 | { 762 | auto it = m_objs.find(handler); 763 | 764 | if(it == m_objs.end()) 765 | throw std::runtime_error("Cannot find handler: " + handler); 766 | 767 | localsPtr = PyModule_GetDict(*(it->second)); 768 | } 769 | 770 | PyErr_Clear(); 771 | CPyObject pyResult = PyRun_String(code.c_str(), eval ? Py_eval_input : Py_file_input, *globals, localsPtr); 772 | if (!*pyResult) 773 | { 774 | handleException(); 775 | throw std::runtime_error("Unknown python error"); 776 | } 777 | 778 | return pyResult; 779 | } 780 | 781 | std::string PyInterpreter::create(const std::string& handler, const std::string& name, CPyObject& args, CPyObject& kwargs) 782 | { 783 | auto obj = call(handler, name, args, kwargs); 784 | 785 | if (obj) 786 | { 787 | auto uuid = getUUID(false, obj); 788 | m_objs[uuid] = obj; 789 | return uuid; 790 | } 791 | 792 | return std::string(); 793 | } 794 | 795 | void PyInterpreter::release(const std::string& handler) 796 | { 797 | if (m_objs.count(handler)) 798 | { 799 | m_imports.erase(*m_objs[handler]); 800 | m_objs.erase(handler); 801 | } 802 | } 803 | 804 | void PyInterpreter::addImportPath(const std::string& path) 805 | { 806 | auto sysPath = PySys_GetObject("path"); 807 | CPyObject dirName = PyUnicode_FromString(path.c_str()); 808 | PyList_Insert(sysPath, 0, *dirName); 809 | } 810 | 811 | namespace 812 | { 813 | std::string normalize(const std::string& s) 814 | { 815 | std::string result; 816 | char lastCh = 'A'; 817 | for (auto& ch : s) 818 | { 819 | if (ch == '\\' || ch == '/') 820 | { 821 | if (lastCh != '\\' && lastCh != '/') 822 | result.push_back('/'); 823 | } 824 | else 825 | result.push_back(ch); 826 | 827 | lastCh = ch; 828 | } 829 | 830 | return result; 831 | } 832 | } 833 | 834 | void PyInterpreter::reimport(const std::string& input) 835 | { 836 | PyErr_Clear(); 837 | 838 | std::vector > reloadThese; 839 | auto directory = ::normalize(input); 840 | 841 | auto sysModules = PySys_GetObject("modules"); 842 | if (!sysModules) 843 | { 844 | handleException(); 845 | throw std::runtime_error("Unknown python error"); 846 | } 847 | 848 | PyObject *key, *pymodule; 849 | Py_ssize_t pos = 0; 850 | while (PyDict_Next(sysModules, &pos, &key, &pymodule)) 851 | { 852 | if (PyModule_Check(pymodule)) 853 | { 854 | CPyObject fileName = PyModule_GetFilenameObject(pymodule); 855 | if (fileName) 856 | { 857 | Py_ssize_t size = 0; 858 | auto str = PyUnicode_AsUTF8AndSize(*fileName, &size); 859 | auto normalized = ::normalize(str); 860 | if (normalized.find(directory) != std::string::npos) 861 | { 862 | auto it = m_imports.find(pymodule); 863 | if (it != m_imports.end()) 864 | reloadThese.push_back({ pymodule, it->second }); 865 | else 866 | reloadThese.push_back({ pymodule, std::string() }); 867 | } 868 | } 869 | } 870 | } 871 | 872 | PyErr_Clear(); 873 | for (auto& reloadThis : reloadThese) 874 | { 875 | CPyObject reloaded = PyImport_ReloadModule(reloadThis.first); 876 | if (!reloaded) 877 | { 878 | handleException(); 879 | return; 880 | } 881 | if (!reloadThis.second.empty()) 882 | { 883 | m_imports[*reloaded] = reloadThis.second; 884 | m_objs[reloadThis.second] = reloaded; 885 | } 886 | } 887 | } 888 | 889 | void PyInterpreter::setSyncJsAndPyInCallback(bool syncJsAndPy) 890 | { 891 | m_syncJsAndPy = syncJsAndPy; 892 | } 893 | -------------------------------------------------------------------------------- /src/pyinterpreter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "cpyobject.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace nodecallspython 11 | { 12 | class GIL 13 | { 14 | PyGILState_STATE m_gstate; 15 | public: 16 | GIL() 17 | { 18 | m_gstate = PyGILState_Ensure(); 19 | } 20 | 21 | ~GIL() 22 | { 23 | PyGILState_Release(m_gstate); 24 | } 25 | 26 | GIL(const GIL&) = delete; 27 | GIL& operator=(const GIL&) = delete; 28 | 29 | GIL(GIL&&) = delete; 30 | GIL& operator=(GIL&&) = delete; 31 | }; 32 | 33 | class PyInterpreter 34 | { 35 | PyThreadState* m_state; 36 | std::unordered_map m_objs; 37 | std::unordered_map m_imports; 38 | bool m_syncJsAndPy; 39 | static std::mutex m_mutex; 40 | static bool m_inited; 41 | public: 42 | PyInterpreter(); 43 | 44 | ~PyInterpreter(); 45 | 46 | std::pair convert(napi_env env, const std::vector& args, bool isSync); 47 | 48 | napi_value convert(napi_env env, PyObject* obj); 49 | 50 | std::string import(const std::string& modulename, bool allowReimport); 51 | 52 | std::string create(const std::string& handler, const std::string& name, CPyObject& args, CPyObject& kwargs); 53 | 54 | void release(const std::string& handler); 55 | 56 | CPyObject call(const std::string& handler, const std::string& func, CPyObject& args, CPyObject& kwargs); 57 | 58 | CPyObject exec(const std::string& handler, const std::string& code, bool eval); 59 | 60 | void addImportPath(const std::string& path); 61 | 62 | void reimport(const std::string& directory); 63 | 64 | void setSyncJsAndPyInCallback(bool syncJsAndPy); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /test/logreg.py: -------------------------------------------------------------------------------- 1 | from sklearn.datasets import load_iris, load_digits 2 | from sklearn.linear_model import LogisticRegression 3 | 4 | class LogReg: 5 | logreg = None 6 | 7 | def __init__(self, dataset): 8 | if (dataset == "iris"): 9 | X, y = load_iris(return_X_y=True) 10 | else: 11 | X, y = load_digits(return_X_y=True) 12 | 13 | self.logreg = LogisticRegression(random_state=42, solver='lbfgs', multi_class='multinomial') 14 | self.logreg.fit(X, y) 15 | 16 | def predict(self, X): 17 | return self.logreg.predict_proba(X).tolist() 18 | -------------------------------------------------------------------------------- /test/nodetest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import nodetestre 3 | import multiprocessing 4 | 5 | def hello(): 6 | print("hello world") 7 | 8 | def dump(a, b): 9 | print(a + b) 10 | 11 | def calc(add, a, b): 12 | if add: 13 | return a + b 14 | else: 15 | return a * b 16 | 17 | def concatenate(s1, s2): 18 | return s1 + s2 19 | 20 | def check(a): 21 | return a == 42 22 | 23 | def multipleNp(a, b): 24 | return np.multiply(a, b); 25 | 26 | def numpyArray(): 27 | return np.array([[1.3, 2.1, 2], [1, 1, 2]]) 28 | 29 | def multiple(a, b): 30 | return np.multiply(a, b).tolist() 31 | 32 | def multiple2D(a, b): 33 | return np.matmul(a, b).tolist() 34 | 35 | def createtuple(): 36 | return ("aaa", 1, 2.3) 37 | 38 | def mergedict(a, b): 39 | res = {} 40 | for k in a: 41 | res[k] = a[k] 42 | 43 | for k in b: 44 | res[k] = b[k] 45 | 46 | return res 47 | 48 | def testException(): 49 | raise RuntimeError("test") 50 | 51 | def undefined(un, n): 52 | return (un, n, {1, 2, "www"}) 53 | 54 | def kwargstest(**kwargs): 55 | return mergedict(kwargs, {"test": 1234}); 56 | 57 | def kwargstestvalue(value, **kwargs): 58 | return mergedict(kwargs, {"test": value}); 59 | 60 | class Calculator: 61 | vector = [] 62 | value = 1 63 | 64 | def __init__(self, vector, **kwargs): 65 | self.vector = vector 66 | if "value" in kwargs: 67 | self.value = kwargs["value"] 68 | 69 | def multiply(self, scalar, vector): 70 | return np.add(np.multiply(scalar * self.value, self.vector), vector).tolist() 71 | 72 | def testReimport(): 73 | return nodetestre.getVar() 74 | 75 | def testBuffer(input, asBA = False): 76 | a = np.frombuffer(input, dtype=np.single) 77 | if asBA: 78 | return bytearray((a * 2).tobytes()) 79 | else: 80 | return (a * 2).tobytes() 81 | 82 | def testBufferEmpty(asBA = False): 83 | if asBA: 84 | return bytearray() 85 | else: 86 | return bytes() 87 | 88 | def testFunction(type, function): 89 | if type == 0: 90 | function() 91 | return 2 92 | else: 93 | function(123, [1, 2, 4], {"a": 1, "b": 2}) 94 | function(125) 95 | return 22 96 | 97 | def testFunctionPromise(type, function): 98 | if type == 0: 99 | res = function() 100 | return res 101 | else: 102 | res = function(123, [1, 2, 4], {"a": 1, "b": 2}) 103 | function(res * 125) 104 | return res * 22 105 | 106 | def compute(i): 107 | return 2 * i 108 | 109 | def testMultiProcessing(len): 110 | numbers = [(i + 1) for i in range(len)] 111 | with multiprocessing.Pool(processes=3) as pool: 112 | results = pool.map(compute, numbers) 113 | return sum(results) 114 | -------------------------------------------------------------------------------- /test/nodetestre.py: -------------------------------------------------------------------------------- 1 | 2 | testVar = 5 3 | 4 | def getVar(): 5 | global testVar 6 | testVar = testVar + 1 7 | return testVar 8 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const nodecallspython = require("../"); 2 | const path = require("path"); 3 | const { Worker } = require('worker_threads'); 4 | 5 | let py = nodecallspython.interpreter; 6 | let pyfile = path.join(__dirname, "nodetest.py"); 7 | 8 | let pymodule = py.importSync(pyfile); 9 | 10 | jest.setTimeout(30000); 11 | 12 | it("nodecallspython tests", async () => { 13 | await py.call(pymodule, "hello"); 14 | await py.call(pymodule, "dump", "a", "b"); 15 | 16 | await expect(py.call(pymodule, "calc", true, 2, 3)).resolves.toEqual(5); 17 | await expect(py.call(pymodule, "calc", false, 2, 3)).resolves.toEqual(6); 18 | 19 | expect(py.callSync(pymodule, "calc", true, 2, 3)).toEqual(5); 20 | expect(py.callSync(pymodule, "calc", false, 2, 3)).toEqual(6); 21 | 22 | await expect(py.call(pymodule, "concatenate", "aaa", "bbb")).resolves.toEqual("aaabbb"); 23 | 24 | expect(py.callSync(pymodule, "concatenate", "aaa", "bbb")).toEqual("aaabbb"); 25 | 26 | await expect(py.call(pymodule, "check", 42)).resolves.toEqual(true); 27 | await expect(py.call(pymodule, "check", 43)).resolves.toEqual(false); 28 | 29 | expect(py.callSync(pymodule, "check", 42)).toEqual(true); 30 | expect(py.callSync(pymodule, "check", 43)).toEqual(false); 31 | 32 | await expect(py.call(pymodule, "multiple", [1, 2, 3, 4], [2, 3, 4, 5])).resolves.toEqual([2, 6, 12, 20]); 33 | 34 | expect(py.callSync(pymodule, "multiple", [1, 2, 3, 4], [2, 3, 4, 5])).toEqual([2, 6, 12, 20]); 35 | 36 | await expect(py.call(pymodule, "multipleNp", [1, 2, 3, 4], [2, 3, 4, 5])).resolves.toEqual([2, 6, 12, 20]); 37 | 38 | expect(py.callSync(pymodule, "multipleNp", [1, 2, 3, 4], [2, 3, 4, 5])).toEqual([2, 6, 12, 20]); 39 | 40 | await expect(py.call(pymodule, "multiple2D", [[1, 2], [3, 4]], [[2, 3], [4, 5]])).resolves.toEqual([[10, 13], [22, 29]]); 41 | 42 | expect(py.callSync(pymodule, "multiple2D", [[1, 2], [3, 4]], [[2, 3], [4, 5]])).toEqual([[10, 13], [22, 29]]); 43 | 44 | await expect(py.call(pymodule, "numpyArray")).resolves.toEqual([[1.3, 2.1, 2], [1, 1, 2]]); 45 | 46 | expect(py.callSync(pymodule, "numpyArray")).toEqual([[1.3, 2.1, 2], [1, 1, 2]]); 47 | 48 | await expect(py.call(pymodule, "createtuple")).resolves.toEqual(["aaa", 1, 2.3 ]); 49 | 50 | expect(py.callSync(pymodule, "createtuple")).toEqual(["aaa", 1, 2.3 ]); 51 | 52 | let res = await py.call(pymodule, "undefined", undefined, null) 53 | expect(res[0]).toEqual(undefined); 54 | expect(res[1]).toEqual(undefined); 55 | expect(res[2].length).toEqual(3); 56 | expect(res[2].includes(1)).toEqual(true); 57 | expect(res[2].includes(2)).toEqual(true); 58 | expect(res[2].includes("www")).toEqual(true); 59 | 60 | let obj1 = { 61 | a: 1, 62 | b: [1, 2, "rrr"], 63 | c: { 64 | test: 34, 65 | array: ["a", "b"] 66 | } 67 | }; 68 | 69 | let obj2 = { 70 | aa: { 71 | "test": 56, 72 | 4: ["a", {a: 3}] 73 | } 74 | }; 75 | 76 | for (var i=0;i<10000;++i) 77 | res = await py.call(pymodule, "mergedict", obj1, obj2); 78 | 79 | expect(i).toEqual(10000); 80 | 81 | obj1["aa"] = obj2["aa"]; 82 | 83 | expect(JSON.stringify(res)).toEqual(JSON.stringify(obj1)); 84 | 85 | for (var i=0;i<10000;++i) 86 | res = py.callSync(pymodule, "mergedict", obj1, obj2); 87 | 88 | expect(i).toEqual(10000); 89 | 90 | expect(JSON.stringify(res)).toEqual(JSON.stringify(obj1)); 91 | 92 | let pyobj; 93 | for (let i=0;i<1000;++i) 94 | pyobj = await py.create(pymodule, "Calculator", [1.4, 5.5, 1.2, 4.4]); 95 | await expect(py.call(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).resolves.toEqual([13.2, 61.5, 12.6, 49.2]); 96 | expect(py.callSync(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).toEqual([13.2, 61.5, 12.6, 49.2]); 97 | 98 | for (let i=0;i<1000;++i) 99 | pyobj = await py.create(pymodule, "Calculator", [1.4, 5.5, 1.2, 4.4], { "value": 2, "__kwargs": true }); 100 | await expect(py.call(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).resolves.toEqual([16, 72.5, 15, 58]); 101 | expect(py.callSync(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).toEqual([16, 72.5, 15, 58]); 102 | 103 | for (let i=0;i<1000;++i) 104 | pyobj = py.createSync(pymodule, "Calculator", [1.4, 5.5, 1.2, 4.4]); 105 | expect(py.callSync(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).toEqual([13.2, 61.5, 12.6, 49.2]); 106 | await expect(py.call(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).resolves.toEqual([13.2, 61.5, 12.6, 49.2]); 107 | 108 | for (let i=0;i<1000;++i) 109 | pyobj = py.createSync(pymodule, "Calculator", [1.4, 5.5, 1.2, 4.4], { "value": 2, "__kwargs": true }); 110 | expect(py.callSync(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).toEqual([16, 72.5, 15, 58]); 111 | await expect(py.call(pyobj, "multiply", 2, [10.4, 50.5, 10.2, 40.4])).resolves.toEqual([16, 72.5, 15, 58]); 112 | 113 | await expect(py.exec(pymodule, "concatenate(\"aaa\", \"bbb\")")).resolves.toEqual(undefined); 114 | expect(py.execSync(pymodule, "concatenate(\"aaa\", \"bbb\")")).toEqual(undefined); 115 | 116 | await expect(py.eval(pymodule, "concatenate(\"aaa\", \"bbb\")")).resolves.toEqual("aaabbb"); 117 | expect(py.evalSync(pymodule, "concatenate(\"aaa\", \"bbb\")")).toEqual("aaabbb"); 118 | 119 | expect(py.callSync(pymodule, "kwargstest", { "obj1": obj1, "__kwargs": true })).toEqual({"obj1": obj1, "test": 1234}); 120 | await expect(py.call(pymodule, "kwargstest", { "obj1": obj1, "__kwargs": true })).resolves.toEqual({"obj1": obj1, "test": 1234}); 121 | 122 | expect(py.callSync(pymodule, "kwargstestvalue", 54321, { "obj1": obj1, "__kwargs": true })).toEqual({"obj1": obj1, "test": 54321}); 123 | await expect(py.call(pymodule, "kwargstestvalue", 54321, {"obj1": obj1, "__kwargs": true })).resolves.toEqual({"obj1": obj1, "test": 54321}); 124 | }); 125 | 126 | it("nodecallspython async import", () => { 127 | expect(py.import(pyfile)).resolves.toMatchObject({handler: expect.stringMatching("@nodecallspython-")}); 128 | }); 129 | 130 | it("nodecallspython errors", async () => { 131 | await expect(py.call(pymodule, "error")).rejects.toEqual("module 'nodetest' has no attribute 'error'"); 132 | expect(() => py.callSync(pymodule, "error")).toThrow("module 'nodetest' has no attribute 'error'"); 133 | 134 | expect(() => py.callSync(pymodule, function(){})).toThrow("Wrong type of arguments"); 135 | 136 | await expect(py.call(pymodule, "dump")).rejects.toEqual("dump() missing 2 required positional arguments: 'a' and 'b'"); 137 | expect(() => py.callSync(pymodule, "dump")).toThrow("dump() missing 2 required positional arguments: 'a' and 'b'"); 138 | 139 | await expect(py.call(pymodule, "dump", "a")).rejects.toEqual("dump() missing 1 required positional argument: 'b'"); 140 | expect(() => py.callSync(pymodule, "dump", "a")).toThrow("dump() missing 1 required positional argument: 'b'"); 141 | 142 | await expect(py.call(pymodule, "testException")).rejects.toMatch(/.+nodetest.py.+RuntimeError: test.*/s); 143 | expect(() => py.callSync(pymodule, "testException")).toThrow(/.+nodetest.py.+RuntimeError: test.*/s); 144 | 145 | await expect(py.create(pymodule, "Calculator2")).rejects.toEqual("module 'nodetest' has no attribute 'Calculator2'"); 146 | expect(() => py.createSync(pymodule, "Calculator2")).toThrow("module 'nodetest' has no attribute 'Calculator2'"); 147 | 148 | await expect(py.import(path.join(__dirname, "error.py"))).rejects.toEqual("No module named 'error'"); 149 | expect(() => py.importSync(path.join(__dirname, "error.py"))).toThrow("No module named 'error'"); 150 | 151 | await expect(py.exec(pymodule, "dump(12)")).rejects.toMatch("dump() missing 1 required positional argument: 'b'"); 152 | expect(() => py.execSync(pymodule, "dump(12)")).toThrow("dump() missing 1 required positional argument: 'b'"); 153 | await expect(py.exec(pymodule, function(){})).rejects.toThrow("Wrong type of arguments"); 154 | expect(() => py.execSync(pymodule, function(){})).toThrow("Wrong type of arguments"); 155 | 156 | await expect(py.eval(pymodule, "dump(12)")).rejects.toMatch("dump() missing 1 required positional argument: 'b'"); 157 | expect(() => py.evalSync(pymodule, "dump(12)")).toThrow("dump() missing 1 required positional argument: 'b'"); 158 | await expect(py.eval(pymodule, function(){})).rejects.toThrow("Wrong type of arguments"); 159 | expect(() => py.evalSync(pymodule, function(){})).toThrow("Wrong type of arguments"); 160 | 161 | async function testError(func, end = 1000) 162 | { 163 | let count = 0; 164 | for (let i=0;i { return py.call(pymodule, "error"); }); 179 | 180 | await testError(() => { return py.call(pymodule, "testException"); }); 181 | 182 | await testError(() => { return py.create(pymodule, "Calculator2"); }); 183 | 184 | await testError(() => { return py.import(path.join(__dirname, "error.py")); }, 10); 185 | 186 | function testErrorSync(func, end = 1000) 187 | { 188 | let count = 0; 189 | for (let i=0;i { return py.callSync(pymodule, "error"); }); 204 | 205 | testErrorSync(() => { return py.callSync(pymodule, "testException"); }); 206 | 207 | testErrorSync(() => { return py.createSync(pymodule, "Calculator2"); }); 208 | 209 | testErrorSync(() => { return py.importSync(path.join(__dirname, "error.py")); }, 10); 210 | }); 211 | 212 | it("nodecallspython worker", () => { 213 | const worker1 = new Worker(path.join(__dirname, "worker.js")); 214 | const worker2 = new Worker(path.join(__dirname, "worker.js")); 215 | }); 216 | 217 | it("nodecallspython import", async () => { 218 | let pymodule = py.importSync(pyfile); 219 | expect(py.importSync(pyfile, false)).not.toEqual(pymodule); 220 | expect(py.importSync(pyfile, true)).not.toEqual(pymodule); 221 | 222 | await expect(py.import(pyfile, false)).resolves.not.toEqual(pymodule); 223 | await expect(py.import(pyfile, true)).resolves.not.toEqual(pymodule); 224 | }); 225 | 226 | it("nodecallspython reimport", () => { 227 | expect(py.callSync(pymodule, "testReimport")).toEqual(6); 228 | expect(py.callSync(pymodule, "testReimport")).toEqual(7); 229 | expect(py.callSync(pymodule, "testReimport")).toEqual(8); 230 | 231 | for (let i=0;i<10;++i) 232 | { 233 | py.reimport(__dirname); 234 | 235 | expect(py.callSync(pymodule, "testReimport")).toEqual(6); 236 | expect(py.callSync(pymodule, "testReimport")).toEqual(7); 237 | } 238 | }); 239 | 240 | it("nodecallspython buffers", () => { 241 | const float32 = new Float32Array(4); 242 | float32[0] = 1.0; 243 | float32[1] = 1.1; 244 | float32[2] = 2.2; 245 | float32[3] = 4.3; 246 | 247 | let result = new Float32Array(py.callSync(pymodule, "testBuffer", float32)); 248 | expect(result.length).toEqual(4); 249 | expect(result[0]).toBeCloseTo(2.0); 250 | expect(result[1]).toBeCloseTo(2.2); 251 | expect(result[2]).toBeCloseTo(4.4); 252 | expect(result[3]).toBeCloseTo(8.6); 253 | 254 | const arrayBuffer = new ArrayBuffer(16); 255 | const float32Buffer = new Float32Array(arrayBuffer); 256 | float32Buffer[0] = 10.0; 257 | float32Buffer[1] = 10.1; 258 | float32Buffer[2] = 20.2; 259 | float32Buffer[3] = 40.3; 260 | 261 | result = new Float32Array(py.callSync(pymodule, "testBuffer", arrayBuffer)); 262 | expect(result.length).toEqual(4); 263 | expect(result[0]).toBeCloseTo(20.0); 264 | expect(result[1]).toBeCloseTo(20.2); 265 | expect(result[2]).toBeCloseTo(40.4); 266 | expect(result[3]).toBeCloseTo(80.6); 267 | 268 | result = new Float32Array(py.callSync(pymodule, "testBuffer", new DataView(arrayBuffer, 4, 8))); 269 | expect(result.length).toEqual(2); 270 | expect(result[0]).toBeCloseTo(20.2); 271 | expect(result[1]).toBeCloseTo(40.4); 272 | 273 | result = new Float32Array(py.callSync(pymodule, "testBuffer", Buffer.from(arrayBuffer, 0, 12))); 274 | expect(result.length).toEqual(3); 275 | expect(result[0]).toBeCloseTo(20.0); 276 | expect(result[1]).toBeCloseTo(20.2); 277 | expect(result[2]).toBeCloseTo(40.4); 278 | 279 | result = new Float32Array(py.callSync(pymodule, "testBuffer", Buffer.from(arrayBuffer, 0, 12), true)); 280 | expect(result.length).toEqual(3); 281 | expect(result[0]).toBeCloseTo(20.0); 282 | expect(result[1]).toBeCloseTo(20.2); 283 | expect(result[2]).toBeCloseTo(40.4); 284 | 285 | result = new Float32Array(py.callSync(pymodule, "testBufferEmpty")); 286 | expect(result.length).toEqual(0); 287 | 288 | result = new Float32Array(py.callSync(pymodule, "testBufferEmpty", true)); 289 | expect(result.length).toEqual(0); 290 | }); 291 | 292 | 293 | it("nodecallspython functions async", async () => { 294 | 295 | py.setSyncJsAndPyInCallback(false); 296 | 297 | for (let i=0;i<1000;++i) 298 | { 299 | let called = false; 300 | function callback() 301 | { 302 | called = true; 303 | } 304 | 305 | expect(py.callSync(pymodule, "testFunction", 0, callback)).toEqual(2); 306 | expect(called).toEqual(true); 307 | expect(await py.call(pymodule, "testFunction", 0, callback)).toEqual(2); 308 | 309 | called = false; 310 | let count = 1; 311 | function callbackArgs(param1, param2, param3) 312 | { 313 | if (param1 == 123) 314 | called = true; 315 | } 316 | 317 | expect(py.callSync(pymodule, "testFunction", 1, callbackArgs)).toEqual(22); 318 | expect(called).toEqual(true); 319 | expect(await py.call(pymodule, "testFunction", 1, callbackArgs)).toEqual(22); 320 | } 321 | }); 322 | 323 | it("nodecallspython functions promise", async () => { 324 | 325 | py.setSyncJsAndPyInCallback(true); 326 | 327 | for (let i=0;i<1000;++i) 328 | { 329 | let count = 0; 330 | function callback() 331 | { 332 | ++count; 333 | return 4; 334 | } 335 | 336 | expect(py.callSync(pymodule, "testFunctionPromise", 0, callback)).toEqual(4); 337 | expect(await py.call(pymodule, "testFunctionPromise", 0, callback)).toEqual(4); 338 | expect(count).toEqual(2); 339 | 340 | count = 0; 341 | function callbackArgs(param1, param2, param3) 342 | { 343 | if (count % 2 == 0) 344 | { 345 | expect(param1).toEqual(123); 346 | expect(param2).toEqual([ 1, 2, 4 ]); 347 | expect(param3).toEqual({ "a": 1, "b": 2 }); 348 | } 349 | else 350 | { 351 | expect(param1).toEqual(375); 352 | expect(param2).toEqual(undefined); 353 | expect(param3).toEqual(undefined); 354 | } 355 | 356 | ++count; 357 | 358 | return 3; 359 | } 360 | 361 | expect(py.callSync(pymodule, "testFunctionPromise", 1, callbackArgs)).toEqual(66); 362 | expect(await py.call(pymodule, "testFunctionPromise", 1, callbackArgs)).toEqual(66); 363 | expect(count).toEqual(4); 364 | } 365 | }); 366 | 367 | it("nodecallspython multiprocessing", async () => { 368 | //py.execSync(pymodule, "import multiprocessing; multiprocessing.set_executable(\"absolute-path-to-python-exe\")") 369 | expect(py.callSync(pymodule, "testMultiProcessing", 5)).toEqual(30); 370 | expect(await py.call(pymodule, "testMultiProcessing", 5)).toEqual(30); 371 | }); 372 | -------------------------------------------------------------------------------- /test/test.mjs: -------------------------------------------------------------------------------- 1 | import { interpreter as py } from 'node-calls-python'; 2 | import py2 from 'node-calls-python'; 3 | 4 | import {join, dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | let pyfile = join(__dirname, "nodetest.py"); 11 | 12 | let pymodule = py.importSync(pyfile); 13 | 14 | console.log(py.callSync(pymodule, "hello")); 15 | console.log(py.callSync(pymodule, "concatenate", "aaa", "bbb")); 16 | -------------------------------------------------------------------------------- /test/testml.js: -------------------------------------------------------------------------------- 1 | const nodecallspython = require("../"); 2 | const path = require("path"); 3 | 4 | let py = nodecallspython.interpreter; 5 | py.developmentMode([__dirname]); 6 | 7 | py.import(path.join(__dirname, "logreg.py")).then(async function(pymodule) { // import the python module 8 | let logreg = await py.create(pymodule, "LogReg", "iris"); // create the instance of the classifier 9 | 10 | let predict = await py.call(logreg, "predict", [[1.4, 5.5, 1.2, 4.4]]); // call predict 11 | console.log(predict); 12 | }); 13 | -------------------------------------------------------------------------------- /test/worker.js: -------------------------------------------------------------------------------- 1 | const nodecallspython = require("../"); 2 | const path = require("path"); 3 | 4 | let py = nodecallspython.interpreter; 5 | let pyfile = path.join(__dirname, "nodetest.py"); 6 | 7 | let pymodule = py.importSync(pyfile); 8 | 9 | console.log(py.evalSync(pymodule, "concatenate(\"aaa\", \"bbb\")")); 10 | 11 | --------------------------------------------------------------------------------