Skip to content

Commit efb2e8b

Browse files
author
Ryan Munro
committed
[#9] start adding typescript support
1 parent 64a589d commit efb2e8b

7 files changed

Lines changed: 451 additions & 19 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
node_modules
2+
package-lock.json
3+
test_typescript.js

README.ts.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
```
6+
npm install python-bridge
7+
```
8+
9+
```typescript
10+
import assert from 'assert';
11+
import { pythonBridge } from 'python-bridge';
12+
13+
async function main() {
14+
const python = pythonBridge();
15+
16+
await python.ex`import math`;
17+
const x = await python`math.sqrt(9)`;
18+
assert.equal(x, 3);
19+
20+
const list = [3, 4, 2, 1];
21+
const sorted = await python`sorted(${list})`;
22+
assert.deepEqual(sorted, list.sort());
23+
24+
await python.end();
25+
}
26+
27+
main().catch(console.error);
28+
```
29+
30+
# API
31+
32+
## var python = pythonBridge(options)
33+
34+
Spawns a Python interpreter, exposing a bridge to the running processing. Configurable via `options`.
35+
36+
* `options.python` - Python interpreter, defaults to `python`
37+
38+
Also inherits the following from [`child_process.spawn([options])`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options).
39+
40+
* `options.cwd` - String Current working directory of the child process
41+
* `options.env` - Object Environment key-value pairs
42+
* `options.stdio` - Array Child's stdio configuration. Defaults to `['pipe', process.stdout, process.stderr]`
43+
* `options.uid` - Number Sets the user identity of the process.
44+
* `options.gid` - Number Sets the group identity of the process.
45+
46+
```javascript
47+
const python = pythonBridge({
48+
python: 'python3',
49+
env: {PYTHONPATH: '/foo/bar'}
50+
});
51+
```
52+
53+
## python`` `expression(args...)` ``.then(...)
54+
55+
Evaluates Python code, returning the value back to Node.
56+
57+
```javascript
58+
// Interpolates arguments using JSON serialization.
59+
assert.deepEqual([1, 3, 4, 6], await python`sorted(${[6, 4, 1, 3]})`);
60+
61+
// Passing key-value arguments
62+
const obj = {hello: 'world', foo: 'bar'};
63+
assert.deepEqual(
64+
{baz: 123, hello: 'world', foo: 'bar'},
65+
await python`dict(baz=123, **${obj})`
66+
);
67+
```
68+
69+
## python.ex`` `statement` ``.then(...)
70+
71+
Execute Python statements.
72+
73+
```javascript
74+
const a = 123, b = 321;
75+
python.ex`
76+
def hello(a, b):
77+
return a + b
78+
`;
79+
assert.equal(a + b, await python`hello(${a}, ${b})`);
80+
```
81+
82+
## python.lock(...).then(...)
83+
84+
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.
85+
86+
```javascript
87+
const x: number = await python.lock(async python =>{
88+
await python.ex`hello = 123`;
89+
return await python`hello + 321`;
90+
});
91+
assert.equal(x, 444);
92+
93+
// Recommended to define function in Python
94+
await python.ex`
95+
def atomic():
96+
hello = 123
97+
return hello + 321
98+
`;
99+
assert.equal(444, await python`atomic()`);
100+
```
101+
102+
## python.stdin, python.stdout, python.stderr
103+
104+
Pipes going into the Python process, separate from execution & evaluation. This can be used to stream data between processes, without buffering.
105+
106+
```javascript
107+
import { delay, promisifyAll } from 'bluebird';
108+
const { createWriteStream, readFileAsync } = promisifyAll(require('fs'));
109+
110+
const fileWriter = createWriteStream('hello.txt');
111+
python.stdout.pipe(fileWriter);
112+
113+
// listen on Python process's stdout
114+
const stdinToStdout = python.ex`
115+
import sys
116+
for line in sys.stdin:
117+
sys.stdout.write(line)
118+
sys.stdout.flush()
119+
`;
120+
121+
// write to Python process's stdin
122+
python.stdin.write('hello\n');
123+
await delay(10);
124+
python.stdin.write('world\n');
125+
126+
// close python's stdin, and wait for python to finish writing
127+
python.stdin.end();
128+
await stdinToStdout;
129+
130+
// assert file contents is the same as what was written
131+
const fileContents = await readFileAsync('hello.txt', {encoding: 'utf8'});
132+
assert.equal(fileContents.replace(/\r/g, ''), 'hello\nworld\n');
133+
```
134+
135+
## python.end()
136+
137+
Stops accepting new Python commands, and waits for queue to finish then gracefully closes the Python process.
138+
139+
## python.disconnect()
140+
141+
_Alias to [`python.end()`](#python-end)_
142+
143+
## python.kill([signal])
144+
145+
Send signal to Python process, same as [`child_process child.kill`](https://nodejs.org/api/child_process.html#child_process_event_exit).
146+
147+
```javascript
148+
import { TimeoutError } from 'bluebird';
149+
150+
let python = pythonBridge();
151+
152+
try {
153+
await python.ex`
154+
from time import sleep
155+
sleep(9000)
156+
`.timeout(100);
157+
assert.ok(false); // should not reach this
158+
} catch (e) {
159+
if (e instanceof TimeoutError) {
160+
python.kill('SIGKILL');
161+
python = pythonBridge();
162+
} else {
163+
throw e;
164+
}
165+
}
166+
python.end();
167+
```
168+
169+
# Handling Exceptions
170+
171+
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.
172+
173+
174+
## python.Exception
175+
176+
Catch any raised Python exception.
177+
178+
```javascript
179+
python.ex`
180+
hello = 123
181+
print(hello + world)
182+
world = 321
183+
`.catch(python.Exception, () => console.log('Woops! `world` was used before it was defined.'));
184+
```
185+
186+
## python.isException(name)
187+
188+
Catch a Python exception matching the passed name.
189+
190+
```javascript
191+
import { isPythonException } from 'python-bridge';
192+
193+
async function pyDivide(numerator, denominator) {
194+
try {
195+
await python`${numerator} / ${denominator}`;
196+
} catch (e) {
197+
if (isPythonException('ZeroDivisionError', e)) {
198+
return Infinity;
199+
}
200+
throw e;
201+
}
202+
}
203+
204+
async function main() {
205+
assert.equal(Infinity, await pyDivide(1, 0));
206+
assert.equal(1 / 0, await pyDivide(1, 0));
207+
}
208+
209+
main().catch(console.error);
210+
```
211+
212+
## pythonBridge.PythonException
213+
214+
_Alias to `python.Exception`, this is useful if you want to import the function to at the root of the module._
215+
216+
## pythonBridge.isPythonException
217+
218+
_Alias to `python.isException`, this is useful if you want to import the function to at the root of the module._
219+
220+
----
221+
222+
# Features
223+
224+
* Does not affect Python's stdin, stdout, or stderr pipes.
225+
* Exception stack traces forwarded to Node for easy debugging.
226+
* Python 2 & 3 support, end-to-end tested.
227+
* Windows support, end-to-end tested.
228+
* Command queueing, with promises.
229+
* Long running Python sessions.
230+
* ES6 template tags for easy interpolation & multiline code.
231+
232+
# Comparisons
233+
234+
After evaluating of the existing landscape of Python bridges, the following issues are why python-bridge was built.
235+
236+
* [python-shell](https://github.com/extrabacon/python-shell) — No promises for queued requests; broken evaluation parser; conflates evaluation and stdout; complex configuration.
237+
* [python](https://github.com/73rhodes/node-python) — Broken evaluation parsing; no exception handling; conflates evaluation, stdout, and stderr.
238+
* [node-python](https://github.com/JeanSebTr/node-python) — Complects execution protocol with incomplete Python embedded DSL.
239+
* [python-runner](https://github.com/teamcarma/node-python-runner) — No long running sessions; `child_process.spawn` wrapper with unintuitive API; no serialization.
240+
* [python.js](https://github.com/monkeycz/python.js) — Embeds specific version of CPython; requires compiler and CPython dev packages; incomplete Python embedded DSL.
241+
* [cpython](https://github.com/eljefedelrodeodeljefe/node-cpython) — Complects execution protocol with incomplete Python embedded DSL.
242+
* [eval.py](https://www.npmjs.com/package/eval.py) — Can only evaluate single line expressions.
243+
* [py.js](https://www.npmjs.com/package/py.js) — For setting up virtualenvs only.
244+
245+
# License
246+
247+
MIT

index.d.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import {
2-
Writable,
3-
Readable,
4-
} from "stream";
5-
61
declare module "python-bridge" {
72
interface pythonBridge extends Function {
8-
(option?: PythonBridgeOptions): PythonBridge;
3+
(options?: PythonBridgeOptions): PythonBridge;
94
}
5+
106
export const pythonBridge: pythonBridge
117

128
export interface PythonBridgeOptions {
13-
intepreter: string;
9+
intepreter?: string;
1410
stdio?: [PipeStdin, PipeStdout, PipeStderr];
1511
cwd?: string;
1612
env?: { [key:string]: string; };
@@ -26,13 +22,14 @@ declare module "python-bridge" {
2622
end(): Promise<void>;
2723
disconnect(): Promise<void>;
2824
kill(signal: string | number): void;
29-
stdin: Writable;
30-
stdout: Readable;
31-
stderr: Readable;
25+
stdin: NodeJS.WritableStream;
26+
stdout: NodeJS.ReadableStream;
27+
stderr: NodeJS.ReadableStream;
3228
connected: boolean;
3329
}
3430

35-
export function isPythonException(e: any): boolean;
31+
export function isPythonException(name: string): (e: any) => boolean;
32+
export function isPythonException(name: string, e: any): boolean;
3633

3734
export class PythonException extends Error {
3835
exception: {
@@ -50,7 +47,7 @@ declare module "python-bridge" {
5047
}
5148

5249
export type Pipe = "pipe" | "ignore" | "inherit";
53-
export type PipeStdin = Pipe | Readable;
54-
export type PipeStdout = Pipe | Writable;
55-
export type PipeStderr = Pipe | Writable;
50+
export type PipeStdin = Pipe | NodeJS.ReadableStream;
51+
export type PipeStdout = Pipe | NodeJS.WritableStream;
52+
export type PipeStderr = Pipe | NodeJS.WritableStream;
5653
}

index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,16 @@ class PythonBridgeNotConnected extends Error {
123123
}
124124
}
125125

126-
function isPythonException(name) {
127-
return exc => (
126+
function isPythonException(name, exc) {
127+
const thunk = exc => (
128128
exc instanceof PythonException &&
129129
exc.exception &&
130130
exc.exception.type.name === name
131131
);
132+
if (exc === undefined) {
133+
return thunk;
134+
}
135+
return thunk(exc);
132136
}
133137

134138
function singleQueue() {
@@ -175,6 +179,7 @@ function json(text_nodes) {
175179
return dedent(text_nodes.reduce((cur, acc, i) => cur + JSON.stringify(values[i - 1]) + acc));
176180
}
177181

182+
pythonBridge.pythonBridge = pythonBridge;
178183
pythonBridge.PythonException = PythonException;
179184
pythonBridge.PythonBridgeNotConnected = PythonBridgeNotConnected;
180185
pythonBridge.isPythonException = isPythonException;

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"lint:ts": "tslint --project tsconfig.json --type-check",
1010
"test": "npm run test:js && npm run test:ts",
1111
"test:js": "tap test.js",
12-
"test:ts": "ts-node test.ts"
12+
"test:ts": "ts-node test.ts",
13+
14+
"test_ts": "ts-node node_modules/blue-tape/bin/blue-tape test_typescript.ts"
1315
},
1416
"repository": {
1517
"type": "git",
@@ -31,10 +33,11 @@
3133
},
3234
"devDependencies": {
3335
"@types/node": "^8.0.14",
34-
"tap": "^2.3.4",
36+
"tap": "^10.7.0",
3537
"temp": "^0.8.3",
3638
"ts-node": "^3.2.0",
3739
"tslint": "^5.5.0",
40+
"blue-tape": "^1.0.0",
3841
"typescript": "^2.4.1"
3942
}
4043
}

0 commit comments

Comments
 (0)