Skip to content

Commit 5d6f5d5

Browse files
author
Ryan Munro
committed
Initial commit
0 parents  commit 5d6f5d5

9 files changed

Lines changed: 1692 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

.travis.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
language: python
2+
os:
3+
- "linux"
4+
# - "osx"
5+
env:
6+
- NODE_VERSION="5.1"
7+
- NODE_VERSION="5.0"
8+
- NODE_VERSION="4.2"
9+
- NODE_VERSION="4.1"
10+
- NODE_VERSION="4.0"
11+
# - NODE_VERSION="0.12"
12+
# - NODE_VERSION="0.10"
13+
python:
14+
- "3.5"
15+
- "3.4"
16+
- "3.3"
17+
- "3.2"
18+
- "2.7"
19+
- "2.6"
20+
before_install:
21+
- rm -rf ~/.nvm
22+
- git clone https://github.com/creationix/nvm.git ~/.nvm
23+
- source ~/.nvm/nvm.sh
24+
- nvm install $NODE_VERSION
25+
install:
26+
- pip install six
27+
- npm install
28+
before_script:
29+
- python -V
30+
- node --version
31+
script:
32+
npm test

README.md

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

TODO.md

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

0 commit comments

Comments
 (0)