├── .gitignore ├── .travis.yml ├── README.md ├── README.ts.md ├── TODO.md ├── appveyor.yml ├── index.d.ts ├── index.js ├── node_python_bridge.py ├── package.json ├── test.js ├── test_typescript.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .DS_Store 3 | .node_repl_history 4 | .npm 5 | node_modules 6 | npm-debug.log* 7 | npm-shrinkwrap.json 8 | package-lock.json 9 | package-lock.json 10 | test_typescript.js 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | os: 3 | - "linux" 4 | env: 5 | - NODE_VERSION="11" 6 | - NODE_VERSION="10" 7 | - NODE_VERSION="9" 8 | - NODE_VERSION="8" 9 | - NODE_VERSION="7.5" 10 | - NODE_VERSION="6.1" 11 | - NODE_VERSION="5.11" 12 | - NODE_VERSION="4.4" 13 | python: 14 | - "3.7-dev" 15 | - "3.6" 16 | - "3.5" 17 | - "3.4" 18 | - "3.3" 19 | - "3.2" 20 | - "2.7" 21 | - "2.6" 22 | before_install: 23 | - rm -rf ~/.nvm 24 | - git clone https://github.com/creationix/nvm.git ~/.nvm 25 | - source ~/.nvm/nvm.sh 26 | - nvm install $NODE_VERSION 27 | install: 28 | - npm install -d 29 | before_script: 30 | - python -V 31 | - node --version 32 | script: 33 | npm test 34 | cache: 35 | directories: 36 | - node_modules 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-bridge [![Build Status](https://secure.travis-ci.org/Submersible/node-python-bridge.png?branch=master)](http://travis-ci.org/Submersible/node-python-bridge) [![Build Status](https://ci.appveyor.com/api/projects/status/8h64yyve684nn900/branch/master?svg=true)](https://ci.appveyor.com/project/munro/node-python-bridge/branch/master) 2 | 3 | Most robust and simple Python bridge. [Features](#features), and [comparisons](#comparisons) to other Python bridges below, supports Windows. 4 | 5 | # API 6 | 7 | [View documentation with TypeScript examples.](README.ts.md) 8 | 9 | ``` 10 | npm install python-bridge 11 | ``` 12 | 13 | ```javascript 14 | 'use strict'; 15 | 16 | let assert = require('assert'); 17 | let pythonBridge = require('python-bridge'); 18 | 19 | let python = pythonBridge(); 20 | 21 | python.ex`import math`; 22 | python`math.sqrt(9)`.then(x => assert.equal(x, 3)); 23 | 24 | let list = [3, 4, 2, 1]; 25 | python`sorted(${list})`.then(x => assert.deepEqual(x, list.sort())); 26 | 27 | python.end(); 28 | ``` 29 | 30 | ## var python = pythonBridge(options) 31 | 32 | Spawns a Python interpreter, exposing a bridge to the running processing. Configurable via `options`. 33 | 34 | * `options.python` - Python interpreter, defaults to `python` 35 | 36 | Also inherits the following from [`child_process.spawn([options])`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options). 37 | 38 | * `options.cwd` - String Current working directory of the child process 39 | * `options.env` - Object Environment key-value pairs 40 | * `options.stdio` - Array Child's stdio configuration. Defaults to `['pipe', process.stdout, process.stderr]` 41 | * `options.uid` - Number Sets the user identity of the process. 42 | * `options.gid` - Number Sets the group identity of the process. 43 | 44 | ```javascript 45 | var python = pythonBridge({ 46 | python: 'python3', 47 | env: {PYTHONPATH: '/foo/bar'} 48 | }); 49 | ``` 50 | 51 | ## python`` `expression(args...)` ``.then(...) 52 | 53 | Evaluates Python code, returning the value back to Node. 54 | 55 | ```javascript 56 | // Interpolates arguments using JSON serialization. 57 | python`sorted(${[6, 4, 1, 3]})`.then(x => assert.deepEqual(x, [1, 3, 4, 6])); 58 | 59 | // Passing key-value arguments 60 | let obj = {hello: 'world', foo: 'bar'}; 61 | python`dict(baz=123, **${obj})`.then(x => { 62 | assert.deepEqual(x, {baz: 123, hello: 'world', foo: 'bar'}); 63 | }); 64 | ``` 65 | 66 | ## python.ex`` `statement` ``.then(...) 67 | 68 | Execute Python statements. 69 | 70 | ```javascript 71 | let a = 123, b = 321; 72 | python.ex` 73 | def hello(a, b): 74 | return a + b 75 | `; 76 | python`hello(${a}, ${b})`.then(x => assert.equal(x, a + b)); 77 | ``` 78 | 79 | ## python.lock(...).then(...) 80 | 81 | Locks access to the Python interpreter so code can be executed atomically. If possible, it's recommend to define a function in Python to handle atomicity. 82 | 83 | ```javascript 84 | python.lock(python => { 85 | python.ex`hello = 123`; 86 | return python`hello + 321'`; 87 | }).then(x => assert.equal(x, 444)); 88 | 89 | // Recommended to define function in Python 90 | python.ex` 91 | def atomic(): 92 | hello = 123 93 | return hello + 321 94 | `; 95 | python`atomic()`.then(x => assert.equal(x, 444)); 96 | ``` 97 | 98 | ## python.stdin, python.stdout, python.stderr 99 | 100 | Pipes going into the Python process, separate from execution & evaluation. This can be used to stream data between processes, without buffering. 101 | 102 | ```javascript 103 | let Promise = require('bluebird'); 104 | let fs = Promise.promisifyAll(require('fs')); 105 | 106 | let fileWriter = fs.createWriteStream('output.txt'); 107 | 108 | python.stdout.pipe(fileWriter); 109 | 110 | // listen on Python process's stdout 111 | python.ex` 112 | import sys 113 | for line in sys.stdin: 114 | sys.stdout.write(line) 115 | sys.stdout.flush() 116 | `.then(function () { 117 | fileWriter.end(); 118 | fs.readFileAsync('output.txt', {encoding: 'utf8'}).then(x => assert.equal(x, 'hello\nworld\n')); 119 | }); 120 | 121 | // write to Python process's stdin 122 | python.stdin.write('hello\n'); 123 | setTimeout(() => { 124 | python.stdin.write('world\n'); 125 | python.stdin.end(); 126 | }, 10); 127 | ``` 128 | 129 | ## python.end() 130 | 131 | Stops accepting new Python commands, and waits for queue to finish then gracefully closes the Python process. 132 | 133 | ## python.disconnect() 134 | 135 | _Alias to [`python.end()`](#python-end)_ 136 | 137 | ## python.kill([signal]) 138 | 139 | Send signal to Python process, same as [`child_process child.kill`](https://nodejs.org/api/child_process.html#child_process_event_exit). 140 | 141 | ```javascript 142 | let Promise = require('bluebird'); 143 | 144 | python.ex` 145 | from time import sleep 146 | sleep(9000) 147 | `.timeout(100).then(x => { 148 | assert.ok(false); 149 | }).catch(Promise.TimeoutError, (exit_code) => { 150 | console.error('Python process taking too long, restarted.'); 151 | python.kill('SIGKILL'); 152 | python = pythonBridge(); 153 | }); 154 | ``` 155 | 156 | # Handling Exceptions 157 | 158 | We can use Bluebird's [`promise.catch(...)`](http://bluebirdjs.com/docs/api/catch.html) catch handler in combination with Python's typed Exceptions to make exception handling easy. 159 | 160 | 161 | ## python.Exception 162 | 163 | Catch any raised Python exception. 164 | 165 | ```javascript 166 | python.ex` 167 | hello = 123 168 | print(hello + world) 169 | world = 321 170 | `.catch(python.Exception, () => console.log('Woops! `world` was used before it was defined.')); 171 | ``` 172 | 173 | ## python.isException(name) 174 | 175 | Catch a Python exception matching the passed name. 176 | 177 | ```javascript 178 | function pyDivide(numerator, denominator) { 179 | return python`${numerator} / ${denominator}` 180 | .catch(python.isException('ZeroDivisionError'), () => Promise.resolve(Infinity)); 181 | } 182 | pyDivide(1, 0).then(x => { 183 | assert.equal(x, Infinity); 184 | assert.equal(1 / 0, Infinity); 185 | }); 186 | ``` 187 | 188 | ## pythonBridge.PythonException 189 | 190 | _Alias to `python.Exception`, this is useful if you want to import the function to at the root of the module._ 191 | 192 | ## pythonBridge.isPythonException 193 | 194 | _Alias to `python.isException`, this is useful if you want to import the function to at the root of the module._ 195 | 196 | ---- 197 | 198 | # Features 199 | 200 | * Does not affect Python's stdin, stdout, or stderr pipes. 201 | * Exception stack traces forwarded to Node for easy debugging. 202 | * Python 2 & 3 support, end-to-end tested. 203 | * Windows support, end-to-end tested. 204 | * Command queueing, with promises. 205 | * Long running Python sessions. 206 | * ES6 template tags for easy interpolation & multiline code. 207 | 208 | # Comparisons 209 | 210 | After evaluating of the existing landscape of Python bridges, the following issues are why python-bridge was built. 211 | 212 | * [python-shell](https://github.com/extrabacon/python-shell) — No promises for queued requests; broken evaluation parser; conflates evaluation and stdout; complex configuration. 213 | * [python](https://github.com/73rhodes/node-python) — Broken evaluation parsing; no exception handling; conflates evaluation, stdout, and stderr. 214 | * [node-python](https://github.com/JeanSebTr/node-python) — Complects execution protocol with incomplete Python embedded DSL. 215 | * [python-runner](https://github.com/teamcarma/node-python-runner) — No long running sessions; `child_process.spawn` wrapper with unintuitive API; no serialization. 216 | * [python.js](https://github.com/monkeycz/python.js) — Embeds specific version of CPython; requires compiler and CPython dev packages; incomplete Python embedded DSL. 217 | * [cpython](https://github.com/eljefedelrodeodeljefe/node-cpython) — Complects execution protocol with incomplete Python embedded DSL. 218 | * [eval.py](https://www.npmjs.com/package/eval.py) — Can only evaluate single line expressions. 219 | * [py.js](https://www.npmjs.com/package/py.js) — For setting up virtualenvs only. 220 | 221 | # License 222 | 223 | MIT 224 | -------------------------------------------------------------------------------- /README.ts.md: -------------------------------------------------------------------------------- 1 | # python-bridge [![Build Status](https://secure.travis-ci.org/Submersible/node-python-bridge.png?branch=master)](http://travis-ci.org/Submersible/node-python-bridge) [![Build Status](https://ci.appveyor.com/api/projects/status/8h64yyve684nn900/branch/master?svg=true)](https://ci.appveyor.com/project/munro/node-python-bridge/branch/master) 2 | 3 | Most robust and simple Python bridge. [Features](#features), and [comparisons](#comparisons) to other Python bridges below, supports Windows. 4 | 5 | # API for TypeScript 6 | 7 | [View documentation with JavaScript examples.](README.ts.md) 8 | 9 | ``` 10 | npm install python-bridge 11 | ``` 12 | 13 | ```typescript 14 | import assert from 'assert'; 15 | import { pythonBridge } from 'python-bridge'; 16 | 17 | async function main() { 18 | const python = pythonBridge(); 19 | 20 | await python.ex`import math`; 21 | const x = await python`math.sqrt(9)`; 22 | assert.equal(x, 3); 23 | 24 | const list = [3, 4, 2, 1]; 25 | const sorted = await python`sorted(${list})`; 26 | assert.deepEqual(sorted, list.sort()); 27 | 28 | await python.end(); 29 | } 30 | 31 | main().catch(console.error); 32 | ``` 33 | 34 | ## var python = pythonBridge(options) 35 | 36 | Spawns a Python interpreter, exposing a bridge to the running processing. Configurable via `options`. 37 | 38 | * `options.python` - Python interpreter, defaults to `python` 39 | 40 | Also inherits the following from [`child_process.spawn([options])`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options). 41 | 42 | * `options.cwd` - String Current working directory of the child process 43 | * `options.env` - Object Environment key-value pairs 44 | * `options.stdio` - Array Child's stdio configuration. Defaults to `['pipe', process.stdout, process.stderr]` 45 | * `options.uid` - Number Sets the user identity of the process. 46 | * `options.gid` - Number Sets the group identity of the process. 47 | 48 | ```javascript 49 | const python = pythonBridge({ 50 | python: 'python3', 51 | env: {PYTHONPATH: '/foo/bar'} 52 | }); 53 | ``` 54 | 55 | ## python`` `expression(args...)` ``.then(...) 56 | 57 | Evaluates Python code, returning the value back to Node. 58 | 59 | ```javascript 60 | // Interpolates arguments using JSON serialization. 61 | assert.deepEqual([1, 3, 4, 6], await python`sorted(${[6, 4, 1, 3]})`); 62 | 63 | // Passing key-value arguments 64 | const obj = {hello: 'world', foo: 'bar'}; 65 | assert.deepEqual( 66 | {baz: 123, hello: 'world', foo: 'bar'}, 67 | await python`dict(baz=123, **${obj})` 68 | ); 69 | ``` 70 | 71 | ## python.ex`` `statement` ``.then(...) 72 | 73 | Execute Python statements. 74 | 75 | ```javascript 76 | const a = 123, b = 321; 77 | python.ex` 78 | def hello(a, b): 79 | return a + b 80 | `; 81 | assert.equal(a + b, await python`hello(${a}, ${b})`); 82 | ``` 83 | 84 | ## python.lock(...).then(...) 85 | 86 | Locks access to the Python interpreter so code can be executed atomically. If possible, it's recommend to define a function in Python to handle atomicity. 87 | 88 | ```javascript 89 | const x: number = await python.lock(async python =>{ 90 | await python.ex`hello = 123`; 91 | return await python`hello + 321`; 92 | }); 93 | assert.equal(x, 444); 94 | 95 | // Recommended to define function in Python 96 | await python.ex` 97 | def atomic(): 98 | hello = 123 99 | return hello + 321 100 | `; 101 | assert.equal(444, await python`atomic()`); 102 | ``` 103 | 104 | ## python.stdin, python.stdout, python.stderr 105 | 106 | Pipes going into the Python process, separate from execution & evaluation. This can be used to stream data between processes, without buffering. 107 | 108 | ```javascript 109 | import { delay, promisifyAll } from 'bluebird'; 110 | const { createWriteStream, readFileAsync } = promisifyAll(require('fs')); 111 | 112 | const fileWriter = createWriteStream('hello.txt'); 113 | python.stdout.pipe(fileWriter); 114 | 115 | // listen on Python process's stdout 116 | const stdinToStdout = python.ex` 117 | import sys 118 | for line in sys.stdin: 119 | sys.stdout.write(line) 120 | sys.stdout.flush() 121 | `; 122 | 123 | // write to Python process's stdin 124 | python.stdin.write('hello\n'); 125 | await delay(10); 126 | python.stdin.write('world\n'); 127 | 128 | // close python's stdin, and wait for python to finish writing 129 | python.stdin.end(); 130 | await stdinToStdout; 131 | 132 | // assert file contents is the same as what was written 133 | const fileContents = await readFileAsync('hello.txt', {encoding: 'utf8'}); 134 | assert.equal(fileContents.replace(/\r/g, ''), 'hello\nworld\n'); 135 | ``` 136 | 137 | ## python.end() 138 | 139 | Stops accepting new Python commands, and waits for queue to finish then gracefully closes the Python process. 140 | 141 | ## python.disconnect() 142 | 143 | _Alias to [`python.end()`](#python-end)_ 144 | 145 | ## python.kill([signal]) 146 | 147 | Send signal to Python process, same as [`child_process child.kill`](https://nodejs.org/api/child_process.html#child_process_event_exit). 148 | 149 | ```javascript 150 | import { TimeoutError } from 'bluebird'; 151 | 152 | let python = pythonBridge(); 153 | 154 | try { 155 | await python.ex` 156 | from time import sleep 157 | sleep(9000) 158 | `.timeout(100); 159 | assert.ok(false); // should not reach this 160 | } catch (e) { 161 | if (e instanceof TimeoutError) { 162 | python.kill('SIGKILL'); 163 | python = pythonBridge(); 164 | } else { 165 | throw e; 166 | } 167 | } 168 | python.end(); 169 | ``` 170 | 171 | # Handling Exceptions 172 | 173 | We can use Bluebird's [`promise.catch(...)`](http://bluebirdjs.com/docs/api/catch.html) catch handler in combination with Python's typed Exceptions to make exception handling easy. 174 | 175 | 176 | ## python.Exception 177 | 178 | Catch any raised Python exception. 179 | 180 | ```javascript 181 | python.ex` 182 | hello = 123 183 | print(hello + world) 184 | world = 321 185 | `.catch(python.Exception, () => console.log('Woops! `world` was used before it was defined.')); 186 | ``` 187 | 188 | ## python.isException(name) 189 | 190 | Catch a Python exception matching the passed name. 191 | 192 | ```javascript 193 | import { isPythonException } from 'python-bridge'; 194 | 195 | async function pyDivide(numerator, denominator) { 196 | try { 197 | await python`${numerator} / ${denominator}`; 198 | } catch (e) { 199 | if (isPythonException('ZeroDivisionError', e)) { 200 | return Infinity; 201 | } 202 | throw e; 203 | } 204 | } 205 | 206 | async function main() { 207 | assert.equal(Infinity, await pyDivide(1, 0)); 208 | assert.equal(1 / 0, await pyDivide(1, 0)); 209 | } 210 | 211 | main().catch(console.error); 212 | ``` 213 | 214 | ## pythonBridge.PythonException 215 | 216 | _Alias to `python.Exception`, this is useful if you want to import the function to at the root of the module._ 217 | 218 | ## pythonBridge.isPythonException 219 | 220 | _Alias to `python.isException`, this is useful if you want to import the function to at the root of the module._ 221 | 222 | ---- 223 | 224 | # Features 225 | 226 | * Does not affect Python's stdin, stdout, or stderr pipes. 227 | * Exception stack traces forwarded to Node for easy debugging. 228 | * Python 2 & 3 support, end-to-end tested. 229 | * Windows support, end-to-end tested. 230 | * Command queueing, with promises. 231 | * Long running Python sessions. 232 | * ES6 template tags for easy interpolation & multiline code. 233 | 234 | # Comparisons 235 | 236 | After evaluating of the existing landscape of Python bridges, the following issues are why python-bridge was built. 237 | 238 | * [python-shell](https://github.com/extrabacon/python-shell) — No promises for queued requests; broken evaluation parser; conflates evaluation and stdout; complex configuration. 239 | * [python](https://github.com/73rhodes/node-python) — Broken evaluation parsing; no exception handling; conflates evaluation, stdout, and stderr. 240 | * [node-python](https://github.com/JeanSebTr/node-python) — Complects execution protocol with incomplete Python embedded DSL. 241 | * [python-runner](https://github.com/teamcarma/node-python-runner) — No long running sessions; `child_process.spawn` wrapper with unintuitive API; no serialization. 242 | * [python.js](https://github.com/monkeycz/python.js) — Embeds specific version of CPython; requires compiler and CPython dev packages; incomplete Python embedded DSL. 243 | * [cpython](https://github.com/eljefedelrodeodeljefe/node-cpython) — Complects execution protocol with incomplete Python embedded DSL. 244 | * [eval.py](https://www.npmjs.com/package/eval.py) — Can only evaluate single line expressions. 245 | * [py.js](https://www.npmjs.com/package/py.js) — For setting up virtualenvs only. 246 | 247 | # License 248 | 249 | MIT 250 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | # Non-ES6 API 4 | 5 | ## python(expression, args...).then(...) 6 | 7 | Evaluates an expression, or calls an expression with arguments. 8 | 9 | ```javascript 10 | python('2 + 2').then((x) => assert.equal(x, 4)); 11 | python('sorted', [6, 4, 1, 3]).then((x) => assert.deepEqual(x, [1, 3, 4, 6])); 12 | ``` 13 | 14 | ## python.ex(statement).then(...) 15 | 16 | Execute a statement that does not return a value. 17 | 18 | ```javascript 19 | python.ex('import math').then(function () { 20 | console.log('Python library `math` imported'); 21 | }); 22 | ``` 23 | 24 | ## python.kw(expression, args..., kwargs).then(...) 25 | 26 | Calls an expression, with arguments, and the last being an object of key-value arguments. 27 | 28 | ```javascript 29 | let obj = {hello: 'world', foo: 'bar'}; 30 | python.kw('dict', obj).then(function (x) { 31 | assert.notStrictEqual(x, obj); 32 | assert.deepEqual(x, obj); 33 | }); 34 | ``` 35 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python27" 4 | - PYTHON: "C:\\Python27-x64" 5 | - PYTHON: "C:\\Python33" 6 | - PYTHON: "C:\\Python33-x64" 7 | - PYTHON: "C:\\Python34" 8 | - PYTHON: "C:\\Python34-x64" 9 | - PYTHON: "C:\\Python35" 10 | - PYTHON: "C:\\Python35-x64" 11 | - PYTHON: "C:\\Python36" 12 | - PYTHON: "C:\\Python36-x64" 13 | 14 | - nodejs_version: "7" 15 | - nodejs_version: "6" 16 | - nodejs_version: "5" 17 | - nodejs_version: "4" 18 | 19 | install: 20 | - ps: Install-Product node $env:nodejs_version 21 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 22 | - "python --version" 23 | - "node --version" 24 | - "npm --version" 25 | - "npm install -d" 26 | 27 | test_script: 28 | - "npm test" 29 | 30 | build: off 31 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface pythonBridge extends Function { 2 | (options?: PythonBridgeOptions): PythonBridge; 3 | } 4 | 5 | export const pythonBridge: pythonBridge 6 | 7 | export interface PythonBridgeOptions { 8 | python?: string; 9 | stdio?: [PipeStdin, PipeStdout, PipeStderr]; 10 | cwd?: string; 11 | env?: { [key: string]: string | undefined; }; 12 | uid?: number; 13 | gid?: number; 14 | } 15 | 16 | export interface PythonBridge { 17 | (literals: TemplateStringsArray | string, ...placeholders: any[]): Bluebird.Promise; 18 | ex(literals: TemplateStringsArray | string, ...placeholders: any[]): Bluebird.Promise; 19 | lock(withLock: (python: PythonBridge) => Promise): Bluebird.Promise 20 | pid: number; 21 | end(): Promise; 22 | disconnect(): Promise; 23 | kill(signal: string | number): void; 24 | stdin: NodeJS.WritableStream; 25 | stdout: NodeJS.ReadableStream; 26 | stderr: NodeJS.ReadableStream; 27 | connected: boolean; 28 | } 29 | 30 | export function isPythonException(name: string): (e: any) => boolean; 31 | export function isPythonException(name: string, e: any): boolean; 32 | 33 | export class PythonException extends Error { 34 | exception: { 35 | message: string; 36 | args: any[]; 37 | type: { name: string; module: string; } 38 | format: string[]; 39 | }; 40 | traceback: { 41 | lineno: number; 42 | strack: string[]; 43 | format: string[] 44 | }; 45 | format: string[] 46 | } 47 | 48 | export type Pipe = "pipe" | "ignore" | "inherit"; 49 | export type PipeStdin = Pipe | NodeJS.ReadableStream; 50 | export type PipeStdout = Pipe | NodeJS.WritableStream; 51 | export type PipeStderr = Pipe | NodeJS.WritableStream; 52 | 53 | export namespace Bluebird { 54 | interface Promise extends _Promise { 55 | timeout(milliseconds: number): Bluebird.Promise; 56 | } 57 | } 58 | 59 | type _Promise = Promise; 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Promise = require('bluebird'); 4 | let path = require('path'); 5 | let child_process = Promise.promisifyAll(require('child_process')); 6 | 7 | const PYTHON_BRIDGE_SCRIPT = path.join(__dirname, 'node_python_bridge.py'); 8 | 9 | function pythonBridge(opts) { 10 | // default options 11 | let intepreter = opts && opts.python || 'python'; 12 | let stdio = opts && opts.stdio || ['pipe', process.stdout, process.stderr]; 13 | let options = { 14 | cwd: opts && opts.cwd, 15 | env: opts && opts.env, 16 | uid: opts && opts.uid, 17 | gid: opts && opts.gid, 18 | stdio: stdio.concat(['ipc']) 19 | }; 20 | 21 | // create process bridge 22 | let ps = child_process.spawn(intepreter, [PYTHON_BRIDGE_SCRIPT], options); 23 | let queue = singleQueue(); 24 | 25 | function sendPythonCommand(type, enqueue, self) { 26 | function wrapper() { 27 | self = self || wrapper; 28 | let code = json.apply(this, arguments); 29 | 30 | if (!(this && this.connected || self.connected)) { 31 | return Promise.reject(new PythonBridgeNotConnected()); 32 | } 33 | 34 | return enqueue(() => new Promise((resolve, reject) => { 35 | ps.send({type: type, code: code}); 36 | ps.once('message', onMessage); 37 | ps.once('close', onClose); 38 | 39 | function onMessage(data) { 40 | ps.removeListener('close', onClose); 41 | if (data && data.type && data.type === 'success') { 42 | resolve(eval(`(${data.value})`)); 43 | } else if (data && data.type && data.type === 'exception') { 44 | reject(new PythonException(data.value)); 45 | } else { 46 | reject(data); 47 | } 48 | } 49 | 50 | function onClose(exit_code, message) { 51 | ps.removeListener('message', onMessage); 52 | if (!message) { 53 | reject(new Error(`Python process closed with exit code ${exit_code}`)); 54 | } else { 55 | reject(new Error(`Python process closed with exit code ${exit_code} and message: ${message}`)); 56 | } 57 | } 58 | })); 59 | } 60 | return wrapper; 61 | } 62 | 63 | function setupLock(enqueue) { 64 | return f => { 65 | return enqueue(() => { 66 | let lock_queue = singleQueue(); 67 | let lock_python = sendPythonCommand('evaluate', lock_queue); 68 | lock_python.ex = sendPythonCommand('execute', lock_queue, lock_python); 69 | lock_python.lock = setupLock(lock_queue); 70 | lock_python.connected = true; 71 | lock_python.__proto__ = python; 72 | 73 | return f(lock_python); 74 | }); 75 | }; 76 | } 77 | 78 | // API 79 | let python = sendPythonCommand('evaluate', queue); 80 | python.ex = sendPythonCommand('execute', queue, python); 81 | python.lock = setupLock(queue); 82 | python.pid = ps.pid; 83 | python.connected = true; 84 | python.Exception = PythonException; 85 | python.isException = isPythonException; 86 | python.disconnect = () => { 87 | python.connected = false; 88 | return queue(() => { 89 | ps.disconnect(); 90 | }); 91 | }; 92 | python.end = python.disconnect; 93 | python.kill = signal => { 94 | python.connected = false; 95 | ps.kill(signal); 96 | }; 97 | python.stdin = ps.stdin; 98 | python.stdout = ps.stdout; 99 | python.stderr = ps.stderr; 100 | return python; 101 | } 102 | 103 | class PythonException extends Error { 104 | constructor(exc) { 105 | if (exc && exc.format) { 106 | super(exc.format.join('')); 107 | } else if (exc && exc.error) { 108 | super(`Python exception: ${exc.error}`); 109 | } else { 110 | super('Unknown Python exception'); 111 | } 112 | this.error = exc.error; 113 | this.exception = exc.exception; 114 | this.traceback = exc.traceback; 115 | this.format = exc.format; 116 | } 117 | } 118 | 119 | class PythonBridgeNotConnected extends Error { 120 | constructor() { 121 | super('Python bridge is no longer connected.'); 122 | } 123 | } 124 | 125 | function isPythonException(name, exc) { 126 | const thunk = exc => ( 127 | exc instanceof PythonException && 128 | exc.exception && 129 | exc.exception.type.name === name 130 | ); 131 | if (exc === undefined) { 132 | return thunk; 133 | } 134 | return thunk(exc); 135 | } 136 | 137 | function singleQueue() { 138 | let last = Promise.resolve(); 139 | return function enqueue(f) { 140 | let wait = last; 141 | let done; 142 | last = new Promise(resolve => { 143 | done = resolve; 144 | }); 145 | return new Promise((resolve, reject) => { 146 | wait.finally(() => { 147 | Promise.try(f).then(resolve, reject); 148 | }); 149 | }).finally(() => done()); 150 | }; 151 | } 152 | 153 | function dedent(code) { 154 | // dedent text 155 | let lines = code.split('\n'); 156 | let offset = null; 157 | 158 | // remove extra blank starting line 159 | if (!lines[0].trim()) { 160 | lines.shift(); 161 | } 162 | for (let line of lines) { 163 | let trimmed = line.trimLeft(); 164 | if (trimmed) { 165 | offset = (line.length - trimmed.length) + 1; 166 | break; 167 | } 168 | } 169 | if (!offset) { 170 | return code; 171 | } 172 | let match = new RegExp('^' + new Array(offset).join('\\s?')); 173 | return lines.map(line => line.replace(match, '')).join('\n'); 174 | } 175 | 176 | function json(text_nodes) { 177 | let values = Array.prototype.slice.call(arguments, 1); 178 | return dedent(text_nodes.reduce((cur, acc, i) => { 179 | return cur + serializePython(values[i - 1]) + acc; 180 | })); 181 | } 182 | 183 | function serializePython(value) { 184 | if (value === null || typeof value === 'undefined') { 185 | return 'None'; 186 | } else if (value === true) { 187 | return 'True'; 188 | } else if (value === false) { 189 | return 'False'; 190 | } else if (value === Infinity) { 191 | return "float('inf')"; 192 | } else if (value === -Infinity) { 193 | return "float('-inf')"; 194 | } else if (value instanceof Array) { 195 | return `[${value.map(serializePython).join(', ')}]`; 196 | } else if (typeof value === 'number') { 197 | if (isNaN(value)) { 198 | return "float('nan')"; 199 | } 200 | return JSON.stringify(value); 201 | } else if (typeof value === 'string') { 202 | return JSON.stringify(value); 203 | } else if (value instanceof Map) { 204 | const props = Array.from(value.entries()).map(kv => `${serializePython(kv[0])}: ${serializePython(kv[1])}`); 205 | return `{${props.join(', ')}}`; 206 | } else { 207 | const props = Object.keys(value).map(k => `${serializePython(k)}: ${serializePython(value[k])}`); 208 | return `{${props.join(', ')}}`; 209 | } 210 | } 211 | 212 | pythonBridge.pythonBridge = pythonBridge; 213 | pythonBridge.PythonException = PythonException; 214 | pythonBridge.PythonBridgeNotConnected = PythonBridgeNotConnected; 215 | pythonBridge.isPythonException = isPythonException; 216 | pythonBridge.json = json; 217 | pythonBridge.serializePython = serializePython; 218 | 219 | module.exports = pythonBridge.pythonBridge = pythonBridge; 220 | -------------------------------------------------------------------------------- /node_python_bridge.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from codeop import Compile 4 | import os 5 | import sys 6 | import json 7 | import traceback 8 | import platform 9 | import struct 10 | import math 11 | 12 | NODE_CHANNEL_FD = int(os.environ['NODE_CHANNEL_FD']) 13 | UNICODE_TYPE = unicode if sys.version_info[0] == 2 else str 14 | 15 | if sys.version_info[0] <= 2: 16 | # print('PY2') 17 | def _exec(_code_, _globs_): 18 | exec('exec _code_ in _globs_') 19 | else: 20 | _exec = getattr(__builtins__, 'exec') 21 | 22 | _locals = {'__name__': '__console__', '__doc__': None} 23 | _compile = Compile() 24 | 25 | 26 | if platform.system() == 'Windows': 27 | # hacky reimplementation of https://github.com/nodejs/node/blob/master/deps/uv/src/win/pipe.c 28 | def read_data(f): 29 | header = f.read(16) 30 | if not header: 31 | return header 32 | try: 33 | msg_length, = struct.unpack(' 0 else '-Infinity' 75 | return o.__dict__ 76 | 77 | 78 | if __name__ == '__main__': 79 | writer = os.fdopen(NODE_CHANNEL_FD, 'wb') 80 | reader = os.fdopen(NODE_CHANNEL_FD, 'rb') 81 | 82 | while True: 83 | try: 84 | # Read new command 85 | line = read_data(reader) 86 | if not line: 87 | break 88 | try: 89 | data = json.loads(line.decode('utf-8')) 90 | except ValueError: 91 | raise ValueError('Could not decode IPC data:\n{}'.format(repr(line))) 92 | 93 | # Assert data saneness 94 | if data['type'] not in ['execute', 'evaluate']: 95 | raise Exception('Python bridge call `type` must be `execute` or `evaluate`') 96 | if not isinstance(data['code'], UNICODE_TYPE): 97 | raise Exception('Python bridge call `code` must be a string.') 98 | 99 | # Run Python code 100 | if data['type'] == 'execute': 101 | _exec(_compile(data['code'], '', 'exec'), _locals) 102 | response = dict(type='success') 103 | else: 104 | value = eval(_compile(data['code'], '', 'eval'), _locals) 105 | response = dict(type='success', value=json.dumps(value, separators=(',', ':'), cls=JavaScriptEncoder)) 106 | except: 107 | t, e, tb = sys.exc_info() 108 | response = dict(type='exception', value=format_exception(t, e, tb)) 109 | 110 | data = json.dumps(response, separators=(',', ':')).encode('utf-8') + b'\n' 111 | write_data(writer, data) 112 | 113 | # Closing is messy 114 | try: 115 | reader.close() 116 | except IOError: 117 | pass 118 | 119 | try: 120 | writer.close() 121 | except IOError: 122 | pass 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-bridge", 3 | "version": "1.1.0", 4 | "description": "Node.js to Python bridge ✨🐍🚀✨", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "lint": "npm run lint:ts", 9 | "lint:ts": "tslint --project tsconfig.json --type-check", 10 | "test": "npm run lint && npm run test:js && npm run test:ts", 11 | "test:js": "tap test.js", 12 | "test:ts": "tsc --lib ES2015 test_typescript.ts && tap test_typescript.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Submersible/node-python-bridge.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/Submersible/node-python-bridge/issues" 20 | }, 21 | "homepage": "https://github.com/Submersible/node-python-bridge#readme", 22 | "keywords": [ 23 | "python", 24 | "bridge", 25 | "ipc" 26 | ], 27 | "author": "Ryan Munro ", 28 | "license": "MIT", 29 | "dependencies": { 30 | "bluebird": "^3.5.0" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^8.0.14", 34 | "tap": "^10.7.0", 35 | "temp": "^0.8.3", 36 | "tslint": "^5.5.0", 37 | "typescript": "^2.4.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let pythonBridge = require('./'); 4 | let PythonException = pythonBridge.PythonException; 5 | let isPythonException = pythonBridge.isPythonException; 6 | let test = require('tap').test; 7 | let Promise = require('bluebird'); 8 | let mkdirTemp = Promise.promisify(require('temp').mkdir); 9 | let path = require('path'); 10 | 11 | test('leave __future__ alone!', t => { 12 | t.plan(2); 13 | 14 | let python = pythonBridge(); 15 | python.ex`import sys`; 16 | python`sys.version_info[0] > 2`.then(py3 => { 17 | python`type('').__name__`.then(x => t.equal(x, 'str')); 18 | python.ex`from __future__ import unicode_literals`; 19 | if (py3) { 20 | python`type('').__name__`.then(x => t.equal(x, 'str')); 21 | } else { 22 | python`type('').__name__`.then(x => t.equal(x, 'unicode')); 23 | } 24 | }).finally(() => { 25 | python.end(); 26 | }); 27 | }); 28 | 29 | test('readme', t => { 30 | t.test('example', t => { 31 | t.plan(2); 32 | 33 | let python = pythonBridge(); 34 | python.ex`import math`; 35 | python`math.sqrt(9)`.then(x => t.equal(x, 3)); 36 | 37 | let list = [3, 4, 2, 1]; 38 | python`sorted(${list})`.then(x => t.deepEqual(x, list.sort())); 39 | 40 | python.end(); 41 | }); 42 | 43 | t.test('expression', t => { 44 | t.plan(2); 45 | 46 | let python = pythonBridge(); 47 | // Interpolates arguments using JSON serialization. 48 | python`sorted(${[6, 4, 1, 3]})`.then(x => t.deepEqual(x, [1, 3, 4, 6])); 49 | 50 | // Passing key-value arguments 51 | let obj = {hello: 'world', foo: 'bar'}; 52 | python`dict(baz=123, **${obj})`.then(x => { 53 | t.deepEqual(x, {baz: 123, hello: 'world', foo: 'bar'}); 54 | }); 55 | python.end(); 56 | }); 57 | 58 | t.test('execute', t => { 59 | t.plan(1); 60 | 61 | let python = pythonBridge(); 62 | let a = 123, b = 321; 63 | python.ex` 64 | def hello(a, b): 65 | return a + b 66 | `; 67 | python`hello(${a}, ${b})`.then(x => t.equal(x, a + b)); 68 | python.end(); 69 | }); 70 | 71 | t.test('lock', t => { 72 | t.plan(3); 73 | 74 | let python = pythonBridge(); 75 | 76 | python.lock(python => { 77 | python.ex`hello = 123`; 78 | let value = python`hello + 321`; 79 | return new Promise(resolve => setTimeout(() => { 80 | python.ex`del hello`.then(() => resolve(value)); 81 | }, 100)); 82 | }).then(x => t.equal(x, 444)); 83 | 84 | python`hello + 321`.catch(isPythonException('NameError'), () => t.ok(true)); 85 | python.ex`hello = 123`; 86 | python`hello + 321`.then(x => t.equal(x, 444)); 87 | 88 | python.disconnect(); 89 | }); 90 | 91 | 92 | t.test('lock recommended', t => { 93 | t.plan(1); 94 | 95 | let python = pythonBridge(); 96 | 97 | python.ex` 98 | def atomic(): 99 | hello = 123 100 | return hello + 321 101 | `; 102 | python`atomic()`.then(x => t.equal(x, 444)); 103 | 104 | python.disconnect(); 105 | }); 106 | 107 | t.test('stdout', t => { 108 | t.plan(1); 109 | let python = pythonBridge({stdio: ['pipe', 'pipe', process.stderr]}); 110 | 111 | mkdirTemp('node-python-bridge-test').then(tempdir => { 112 | const OUTPUT = path.join(tempdir, 'output.txt'); 113 | 114 | let Promise = require('bluebird'); 115 | let fs = Promise.promisifyAll(require('fs')); 116 | 117 | let fileWriter = fs.createWriteStream(OUTPUT); 118 | 119 | python.stdout.pipe(fileWriter); 120 | 121 | // listen on Python process's stdout 122 | python.ex` 123 | import sys 124 | for line in sys.stdin: 125 | sys.stdout.write(line) 126 | sys.stdout.flush() 127 | `.then(function () { 128 | fileWriter.end(); 129 | fs.readFileAsync(OUTPUT, {encoding: 'utf8'}).then(x => { 130 | t.equal(x.replace(/\r/g, ''), 'hello\nworld\n') 131 | }); 132 | }); 133 | 134 | // write to Python process's stdin 135 | python.stdin.write('hello\n'); 136 | setTimeout(() => { 137 | python.stdin.write('world\n'); 138 | python.stdin.end(); 139 | }, 10); 140 | 141 | python.end(); 142 | }); 143 | }); 144 | 145 | t.test('kill', t => { 146 | t.plan(2); 147 | 148 | let python = pythonBridge(); 149 | 150 | let Promise = require('bluebird'); 151 | 152 | python.ex` 153 | from time import sleep 154 | sleep(9000) 155 | `.timeout(100).then(x => { 156 | t.ok(false); 157 | }).catch(Promise.TimeoutError, exit_code => { 158 | python.kill('SIGKILL'); 159 | t.ok(true); 160 | python = pythonBridge(); 161 | }); 162 | setTimeout(() => { 163 | python`1 + 2`.then(x => t.equal(x, 3)); 164 | python.disconnect(); 165 | }, 200); 166 | 167 | // python.disconnect(); 168 | }); 169 | 170 | t.test('exceptions', t => { 171 | t.plan(6); 172 | 173 | let python = pythonBridge(); 174 | 175 | python.ex` 176 | hello = 123 177 | print(hello + world) 178 | world = 321 179 | `.catch(python.Exception, () => t.ok(true)); 180 | 181 | python.ex` 182 | hello = 123 183 | print(hello + world) 184 | world = 321 185 | `.catch(pythonBridge.PythonException, () => t.ok(true)); 186 | 187 | function pyDivide(numerator, denominator) { 188 | return python`${numerator} / ${denominator}` 189 | .catch(python.isException('ZeroDivisionError'), () => Promise.resolve(Infinity)); 190 | } 191 | pyDivide(1, 0).then(x => { 192 | t.equal(x, Infinity); 193 | t.equal(1 / 0, Infinity); 194 | }); 195 | pyDivide(6, 2).then(x => t.equal(x, 3)); 196 | 197 | python`1 / 0` 198 | .catch(pythonBridge.isPythonException('ZeroDivisionError'), () => Promise.resolve(Infinity)) 199 | .then(x => t.equal(x, 1 / 0)); 200 | 201 | python.disconnect(); 202 | }); 203 | 204 | t.end(); 205 | }); 206 | 207 | test('nested locks', t => { 208 | t.plan(3); 209 | 210 | let python = pythonBridge(); 211 | 212 | python.lock(python => { 213 | python.ex`hello = 123`; 214 | let $value1 = python`hello + 321`; 215 | let $value2 = python.lock(python => { 216 | python.ex`world = 808`; 217 | return python`world + 191`; 218 | }); 219 | return new Promise(resolve => setTimeout(() => { 220 | python.ex`del hello`.then(() => { 221 | return Promise.all([$value1, $value2]).spread((value1, value2) => { 222 | resolve(value1 + value2); 223 | }) 224 | }); 225 | }, 100)); 226 | }).then(x => t.equal(x, 1443)); 227 | 228 | python`hello + 808`.catch(isPythonException('NameError'), () => t.ok(true)); 229 | python.ex`hello = 123`; 230 | python`hello + 321`.then(x => t.equal(x, 444)); 231 | 232 | python.disconnect(); 233 | }); 234 | 235 | test('exceptions', t => { 236 | t.plan(3); 237 | 238 | let python = pythonBridge(); 239 | python`1 / 0`.catch(() => t.ok(true)); 240 | python`1 / 0` 241 | .catch(ReferenceError, () => t.ok(false)) 242 | .catch(PythonException, () => t.ok(true)); 243 | python`1 / 0` 244 | .catch(isPythonException('IOError'), () => t.ok(false)) 245 | .catch(isPythonException('ZeroDivisionError'), () => t.ok(true)); 246 | python.end(); 247 | }); 248 | 249 | test('json interpolation', t => { 250 | t.equal(pythonBridge.json` 251 | def hello(a, b): 252 | return a + b 253 | `, 'def hello(a, b):\n return a + b\n'); 254 | t.equal(pythonBridge.json`hello()`, 'hello()'); 255 | t.equal(pythonBridge.json`hello(${'world'})`, 'hello("world")'); 256 | t.equal(pythonBridge.json`hello(${'world'}, ${[1, 2, 3]})`, 'hello("world", [1, 2, 3])'); 257 | t.equal(pythonBridge.json`hello(${new Map([[1, 2], [3, 4]])})`, 'hello({1: 2, 3: 4})'); 258 | t.equal(pythonBridge.json`hello(${NaN}, ${Infinity}, ${-Infinity})`, "hello(float('nan'), float('inf'), float('-inf'))"); 259 | t.end(); 260 | }); 261 | 262 | test('bug #22 returning NaN or infinity does not work', t => { 263 | t.plan(1); 264 | const s = {a: NaN, b: Infinity, c: -Infinity}; 265 | const python = pythonBridge(); 266 | python`(lambda x: x)(${s})`.then(x => t.deepEqual(x, s)); 267 | python.end(); 268 | }); 269 | 270 | test('bug #24 support more than just numbers and strings', t => { 271 | t.plan(1); 272 | const s = {a: 'asdf', b: 1, c: true, d: [1, 2, null]}; 273 | const python = pythonBridge(); 274 | python`(lambda x: x)(${s})`.then(x => t.deepEqual(x, s)); 275 | python.end(); 276 | }); 277 | -------------------------------------------------------------------------------- /test_typescript.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap'; 2 | import { join as path_join } from 'path'; 3 | import { promisify } from 'bluebird'; 4 | import { pythonBridge, PythonException, isPythonException } from './index'; 5 | 6 | const mkdirTemp = promisify(require('temp').mkdir); 7 | 8 | test('readme', t => { 9 | t.test('example', async assert => { 10 | const python = pythonBridge(); 11 | try { 12 | await python.ex`import math`; 13 | const x = await python`math.sqrt(9)`; 14 | assert.equal(x, 3); 15 | 16 | const list = [3, 4, 2, 1]; 17 | const sorted = await python`sorted(${list})`; 18 | assert.deepEqual(sorted, list.sort()); 19 | } finally { 20 | python.end(); 21 | } 22 | }); 23 | 24 | t.test('expression', async assert => { 25 | let python = pythonBridge(); 26 | try { 27 | // Interpolates arguments using JSON serialization. 28 | assert.deepEqual([1, 3, 4, 6], await python`sorted(${[6, 4, 1, 3]})`); 29 | 30 | // Passing key-value arguments 31 | const obj = {hello: 'world', foo: 'bar'}; 32 | assert.deepEqual( 33 | {baz: 123, hello: 'world', foo: 'bar'}, 34 | await python`dict(baz=123, **${obj})` 35 | ); 36 | } finally { 37 | python.end(); 38 | } 39 | }); 40 | 41 | t.test('execute', async assert => { 42 | const python = pythonBridge(); 43 | try { 44 | const a = 123, b = 321; 45 | python.ex` 46 | def hello(a, b): 47 | return a + b 48 | `; 49 | assert.equal(a + b, await python`hello(${a}, ${b})`); 50 | } finally { 51 | python.end(); 52 | } 53 | }); 54 | 55 | t.test('lock', async assert => { 56 | const python = pythonBridge(); 57 | try { 58 | const x: number = await python.lock(async python =>{ 59 | await python.ex`hello = 123`; 60 | return await python`hello + 321`; 61 | }); 62 | assert.equal(x, 444); 63 | 64 | // Recommended to define function in Python 65 | } finally { 66 | python.end(); 67 | } 68 | }); 69 | 70 | t.test('lock recommended', async assert => { 71 | const python = pythonBridge(); 72 | try { 73 | const x: number = await python.lock(async python =>{ 74 | await python.ex`hello = 123`; 75 | return await python`hello + 321`; 76 | }); 77 | assert.equal(x, 444); 78 | } finally { 79 | 80 | python.end(); 81 | } 82 | }); 83 | 84 | 85 | t.test('stdout', async assert => { 86 | const python = pythonBridge({stdio: ['pipe', 'pipe', process.stderr]}) 87 | 88 | try { 89 | const tempdir = await mkdirTemp('node-python-bridge-test'); 90 | const OUTPUT = path_join(tempdir, 'output.txt'); 91 | 92 | const { delay, promisifyAll } = require('bluebird'); 93 | const { createWriteStream, readFileAsync } = promisifyAll(require('fs')); 94 | const fileWriter = createWriteStream(OUTPUT); 95 | 96 | python.stdout.pipe(fileWriter); 97 | 98 | // listen on Python process's stdout 99 | const stdinToStdout = python.ex` 100 | import sys 101 | for line in sys.stdin: 102 | sys.stdout.write(line) 103 | sys.stdout.flush() 104 | `; 105 | 106 | // write to Python process's stdin 107 | python.stdin.write('hello\n'); 108 | await delay(10); 109 | python.stdin.write('world\n'); 110 | 111 | // close python's stdin, and wait for python to finish writing 112 | python.stdin.end(); 113 | await stdinToStdout; 114 | 115 | // assert file contents is the same as what was written 116 | const fileContents = await readFileAsync(OUTPUT, {encoding: 'utf8'}); 117 | assert.equal(fileContents.replace(/\r/g, ''), 'hello\nworld\n'); 118 | } finally { 119 | python.end(); 120 | } 121 | }); 122 | 123 | t.test('kill', async assert => { 124 | 125 | let python = pythonBridge(); 126 | 127 | let {TimeoutError} = require('bluebird'); 128 | 129 | try { 130 | await python.ex` 131 | from time import sleep 132 | sleep(9000) 133 | `.timeout(100); 134 | assert.ok(false); // should not reach this 135 | } catch (e) { 136 | if (e instanceof TimeoutError) { 137 | python.kill('SIGKILL'); 138 | python = pythonBridge(); 139 | } else { 140 | throw e; 141 | } 142 | } 143 | python.end(); 144 | }); 145 | 146 | t.test('exceptions', async assert => { 147 | let python = pythonBridge(); 148 | 149 | try { 150 | await python.ex` 151 | hello = 123 152 | print(hello + world) 153 | world = 321 154 | `; 155 | assert.ok(false); 156 | } catch (e) { 157 | assert.ok(e instanceof PythonException); 158 | } 159 | 160 | async function pyDivide(numerator, denominator) { 161 | try { 162 | await python`${numerator} / ${denominator}`; 163 | } catch (e) { 164 | if (isPythonException('ZeroDivisionError', e)) { 165 | return Infinity; 166 | } 167 | throw e; 168 | } 169 | } 170 | 171 | async function main() { 172 | assert.equal(Infinity, await pyDivide(1, 0)); 173 | assert.equal(1 / 0, await pyDivide(1, 0)); 174 | } 175 | await main(); 176 | 177 | python.end(); 178 | }); 179 | 180 | t.end(); 181 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" 4 | , "lib": ["es2015"] 5 | , "module": "commonjs" 6 | , "outDir": "dist" 7 | , "declaration": true 8 | , "sourceMap": true 9 | , "noEmitOnError": true 10 | , "noUnusedLocals": true 11 | , "noImplicitReturns": true 12 | , "noFallthroughCasesInSwitch": true 13 | , "strictNullChecks": true 14 | , "noImplicitAny": true 15 | , "noUnusedParameters": false 16 | , "noImplicitThis": true 17 | , "noLib": false 18 | , "traceResolution": false 19 | } 20 | , "exclude": [ 21 | "node_modules/" 22 | , "dist/" 23 | ] 24 | , "include": [ 25 | "test.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "statements" 7 | ], 8 | "jsdoc-require": [ 9 | false 10 | ], 11 | "ban": false, 12 | "class-name": true, 13 | "comment-format": [ 14 | true, 15 | "check-space" 16 | ], 17 | "curly": false, 18 | "eofline": true, 19 | "forin": false, 20 | "indent": [ 21 | true, 22 | "spaces" 23 | ], 24 | "interface-name": [false], 25 | "jsdoc-format": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 180 30 | ], 31 | "callable-types": true, 32 | "import-blacklist": [true, "rxjs"], 33 | "interface-over-type-literal": true, 34 | "no-empty-interface": true, 35 | "no-string-throw": true, 36 | "prefer-const": true, 37 | "typeof-compare": true, 38 | "unified-signatures": false, 39 | "no-inferrable-types": [true, "ignore-params"], 40 | "member-access": true, 41 | "member-ordering": [false], 42 | "no-any": false, 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-conditional-assignment": true, 46 | "no-consecutive-blank-lines": [ 47 | true 48 | ], 49 | "no-console": [false], 50 | "no-construct": false, 51 | "no-debugger": true, 52 | "no-duplicate-variable": true, 53 | "no-empty": true, 54 | "no-eval": true, 55 | "no-internal-module": true, 56 | "no-require-imports": false, 57 | "no-shadowed-variable": true, 58 | "no-string-literal": false, 59 | "no-switch-case-fall-through": true, 60 | "no-trailing-whitespace": true, 61 | "no-unused-expression": true, 62 | "no-use-before-declare": true, 63 | "no-var-keyword": true, 64 | "no-var-requires": false, 65 | "object-literal-sort-keys": false, 66 | "one-line": [ 67 | true, 68 | "check-open-brace", 69 | "check-whitespace" 70 | ], 71 | "quotemark": [ 72 | true, 73 | "single", 74 | "avoid-escape" 75 | ], 76 | "radix": false, 77 | "semicolon": [ 78 | false 79 | ], 80 | "switch-default": false, 81 | "trailing-comma": [ 82 | true, 83 | { 84 | "multiline": "always", 85 | "singleline": "never" 86 | } 87 | ], 88 | "triple-equals": [true], 89 | "typedef": [false], 90 | "typedef-whitespace": [ 91 | true, 92 | { 93 | "call-signature": "nospace", 94 | "index-signature": "nospace", 95 | "parameter": "nospace", 96 | "property-declaration": "nospace", 97 | "variable-declaration": "nospace" 98 | } 99 | ], 100 | "variable-name": [ 101 | true, 102 | "check-format", 103 | "allow-leading-underscore", 104 | "ban-keywords" 105 | ], 106 | "whitespace": [ 107 | true, 108 | "check-branch", 109 | "check-decl", 110 | "check-operator", 111 | "check-separator", 112 | "check-type" 113 | ] 114 | } 115 | } 116 | --------------------------------------------------------------------------------