Skip to content

Commit 87bb561

Browse files
author
Ryan Munro
authored
Merge pull request #11 from Submersible/typescript
Add Typescript Type Definition
2 parents 02051c7 + 28419b0 commit 87bb561

10 files changed

Lines changed: 666 additions & 13 deletions

File tree

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1+
.*.swp
2+
.DS_Store
3+
.node_repl_history
4+
.npm
15
node_modules
6+
npm-debug.log*
7+
npm-shrinkwrap.json
8+
package-lock.json
9+
package-lock.json
10+
test_typescript.js

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Most robust and simple Python bridge. [Features](#features), and [comparisons](#comparisons) to other Python bridges below, supports Windows.
44

5+
# API
6+
7+
[View documentation for TypeScript examples.](README.ts.md)
8+
59
```
610
npm install python-bridge
711
```
@@ -23,8 +27,6 @@ python`sorted(${list})`.then(x => assert.deepEqual(x, list.sort()));
2327
python.end();
2428
```
2529

26-
# API
27-
2830
## var python = pythonBridge(options)
2931

3032
Spawns a Python interpreter, exposing a bridge to the running processing. Configurable via `options`.
@@ -76,7 +78,7 @@ python`hello(${a}, ${b})`.then(x => assert.equal(x, a + b));
7678

7779
## python.lock(...).then(...)
7880

79-
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.
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.
8082

8183
```javascript
8284
python.lock(python => {

README.ts.md

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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 TypeScript 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

appveyor.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,3 @@ test_script:
2828
- "npm test"
2929

3030
build: off
31-
32-
cache:
33-
- node_modules

index.d.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
interface pythonBridge extends Function {
2+
(options?: PythonBridgeOptions): PythonBridge;
3+
}
4+
5+
export const pythonBridge: pythonBridge
6+
7+
export interface PythonBridgeOptions {
8+
intepreter?: string;
9+
stdio?: [PipeStdin, PipeStdout, PipeStderr];
10+
cwd?: string;
11+
env?: { [key:string]: string; };
12+
uid?: number;
13+
gid?: number;
14+
}
15+
16+
export interface PythonBridge {
17+
(literals: TemplateStringsArray | string, ...placeholders: any[]): Bluebird.Promise<any>;
18+
ex(literals: TemplateStringsArray | string, ...placeholders: any[]): Bluebird.Promise<void>;
19+
lock<T>(withLock: (python: PythonBridge) => Promise<T>): Bluebird.Promise<T>
20+
pid: number;
21+
end(): Promise<void>;
22+
disconnect(): Promise<void>;
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<T> extends _Promise<T> {
55+
timeout(number): Bluebird.Promise<T>;
56+
}
57+
}
58+
59+
type _Promise<T> = Promise<T>;

index.js

Lines changed: 9 additions & 3 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,8 +179,10 @@ 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;
181186
pythonBridge.json = json;
182-
module.exports = pythonBridge;
187+
188+
module.exports = pythonBridge.pythonBridge = pythonBridge;

0 commit comments

Comments
 (0)