Skip to content

Commit f4b6ab4

Browse files
authored
Louis/python-migration (#111)
* Add python-interactive dependency * Remove PythonShell And its dependencies. * Convert PythonShell to PythonInteractive * Add docstrings to python.js exports * Fix broken tests * Remove redundant script test Also refactor some other stuff. * Add check to see if venv exists Before starting the Python shell instance, check if the venv exists. If yes then use it. If no then create it by running pyvenv.sh. * Update README.md Add Bash to prerequisites. * Fix failing tests
1 parent a2704f7 commit f4b6ab4

File tree

31 files changed

+515
-1214
lines changed

31 files changed

+515
-1214
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ For the latest changes, please read the [CHANGELOG](CHANGELOG.md).
3030
- [Acknowledgements](#acknowledgements)
3131

3232
## Prerequisites
33-
Node-RED Quantum requires at minimum [Node.js 12.0.0](https://nodejs.org/en/), [Node-RED 1.0](https://nodered.org), and [Python 3](https://www.python.org/).
33+
Node-RED Quantum requires at minimum [Node.js 12.0.0](https://nodejs.org/en/), [Node-RED 1.0](https://nodered.org), and [Python 3](https://www.python.org/). It is also advised that you start Node-RED from a Bash shell, as it is required to prepare the Python virtual environment.
3434

3535
## Installation
3636
1. Install Node-RED locally by following the installation instructions [here](https://nodered.org/docs/getting-started/local).

nodes/python.js

Lines changed: 19 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -2,180 +2,42 @@
22

33
const os = require('os');
44
const path = require('path');
5-
const fileSystem = require('fs');
5+
const util = require('util');
6+
const exec = util.promisify(require('child_process').exec);
67
const pythonExecutable = os.platform() === 'win32' ? 'venv/Scripts/python.exe' : 'venv/bin/python';
78
const pythonPath = path.resolve(__dirname + '/..', pythonExecutable);
8-
const childProcess = require('child_process');
9-
const replaceAll = require('string.prototype.replaceall');
10-
const Mutex = require('async-mutex').Mutex;
11-
const mutex = new Mutex();
9+
const {PythonInteractive} = require('python-interactive');
1210

1311

14-
async function createPromise(process) {
15-
return new Promise((resolve, reject) => {
16-
let stdoutData = '';
17-
let stderrData = '';
18-
let stdoutDone = false;
19-
let stderrDone = false;
20-
21-
let done = function() {
22-
process.stdout.removeAllListeners();
23-
process.stderr.removeAllListeners();
24-
if (stderrData.trim()) {
25-
reject(stderrData.trim());
26-
} else {
27-
resolve(stdoutData.trim());
28-
}
29-
};
30-
31-
process.stdout.on('data', function(data) {
32-
stdoutData += data;
33-
if (stdoutData.includes('#StdoutEnd#')) {
34-
stdoutData = stdoutData.replace('#StdoutEnd#', '');
35-
stdoutDone = true;
36-
if (stdoutDone && stderrDone) {
37-
done();
38-
}
39-
}
40-
});
41-
42-
process.stderr.on('data', function(data) {
43-
stderrData += data;
44-
if (stderrData.includes('#StderrEnd#')) {
45-
stderrData = replaceAll(stderrData, '>>>', '');
46-
stderrData = replaceAll(stderrData, '...', '');
47-
stderrData = stderrData.replace('#StderrEnd#', '');
48-
stderrDone = true;
49-
50-
// Remove '>' if it is only character in the string
51-
if (stderrData.includes('>') && !replaceAll(stderrData, '>', '').trim()) {
52-
stderrData = replaceAll(stderrData, '>', '').trim();
53-
}
54-
55-
if (stdoutDone && stderrDone) {
56-
done();
57-
}
58-
}
59-
});
60-
});
12+
async function createVirtualEnvironment() {
13+
const bashPath = path.resolve(__dirname + '/../bin/pyvenv.sh');
14+
return exec(`bash ${bashPath}`);
6115
}
6216

63-
class PythonShell {
64-
/**
65-
* Initialises a new PythonShell instance.
66-
*
67-
* Each instance of PythonShell spawns its own shell, separate from all other instances.
68-
* @param {string} path Location of the Python executable. Uses venv executable by default.
69-
*/
70-
constructor(path) {
71-
this.path = path ? path : pythonPath;
72-
this.script = '';
73-
this.process = null;
74-
this.lastCommand = '';
75-
}
7617

18+
module.exports = {
7719
/**
78-
* Executes a string of Python code and returns the output via a Promise.
79-
*
80-
* Calls to this method must be done asynchronously through the use of 'async' and 'await'.
20+
* The path to the Python executable.
8121
*
82-
* @param {string} command Python command(s) to be executed. May be a single command or
83-
* multiple commands which are separated by a new line. If undefined, an empty line is executed.
84-
* @param {function(string, string):void} callback Callback function to be run on completion.
85-
* If command execution was succesful, arg1 of the callback function is the result and arg0 is
86-
* null. If the command returned an error, arg1 of the callback function is null and arg0 is the
87-
* error message. If undefined, the output is returned by the Promise, and any errors are returned
88-
* as strings rather than Error objects.
89-
* @return {Promise<string>} Returns a Promise object which will run the callback function,
90-
* passing the command output as a parameter. If the command is successful the Promise is
91-
* resolved, otherwise it is rejected.
92-
* @throws {Error} Throws an Error if the Python process has not been started.
22+
* This path points to the Python virtual environment and adapts depending on the platform.
9323
*/
94-
async execute(command, callback) {
95-
return mutex.runExclusive(async () => {
96-
if (!this.process) {
97-
throw new Error('Python process has not been started - call start() before executing commands.');
98-
}
99-
100-
command = command ? command : '';
101-
this.lastCommand = command;
102-
command = '\n' + command + '\n';
103-
this.script += command;
104-
command += '\nprint("#StdoutEnd#")\n';
105-
command += 'from sys import stderr; print("#StderrEnd#", file=stderr)\n';
106-
107-
let promise = createPromise(this.process)
108-
.then((data) => callback !== undefined ? callback(null, data) : data)
109-
.catch((err) => callback !== undefined ? callback(err, null) : err);
110-
this.process.stdin.write(command);
111-
112-
return promise;
113-
});
114-
}
24+
PythonPath: pythonPath,
11525

11626
/**
117-
* Spawns a new Python process.
27+
* The global Python shell instance.
11828
*
119-
* This method will only execute and return if there is no process currently running. To end the
120-
* old process, call the stop() method.
121-
*
122-
* @return {Promise<string>} Returns a Promise object which contains Python interpreter
123-
* and system information. If not required, this can be ignored.
124-
* @throws {Error} Throws an Error object if path to the Python executable cannot be found.
29+
* This shell instance will be maintained throughout the entire lifetime of a flow. Any variables,
30+
* functions, and objects which are created will be kept in memory until the flow ends.
12531
*/
126-
async start() {
127-
if (!this.process) {
128-
if (!fileSystem.existsSync(this.path)) {
129-
throw new Error(`cannot resolve path for Python executable: ${this.path}`);
130-
}
131-
this.process = childProcess.spawn(this.path, ['-u', '-i']);
132-
this.process.stdout.setEncoding('utf8');
133-
this.process.stderr.setEncoding('utf8');
134-
this.process.stdout.setMaxListeners(1);
135-
this.process.stderr.setMaxListeners(1);
136-
return this.execute();
137-
}
138-
}
32+
PythonShell: new PythonInteractive(pythonPath),
13933

14034
/**
141-
* End a currently running Python process.
142-
*
143-
* This method will only execute if there is a process currently running. To start a new process,
144-
* call the start() method.
35+
* Class definition for creating a Python shell instance.
14536
*/
146-
stop() {
147-
if (this.process) {
148-
this.process.stdin.destroy();
149-
this.process.stdout.destroy();
150-
this.process.stderr.destroy();
151-
this.process.kill();
152-
this.process = null;
153-
this.script = '';
154-
this.lastCommand = '';
155-
}
156-
}
37+
PythonInteractive: PythonInteractive,
15738

15839
/**
159-
* End the current Python process and start a new one.
160-
*
161-
* This method acts as a wrapper for executing stop() and then start(). It will only stop a
162-
* process if there is a process currently running. If not, then only a new process is started.
163-
*
164-
* @return {Promise<string>} Returns a Promise object which contains Python interpreter
165-
* and system information. If not required, this can be ignored.
166-
* @throws {Error} Throws an Error if the Python executable cannot be found.
40+
* Create a new Python virtual environment.
16741
*/
168-
async restart() {
169-
this.stop();
170-
return this.start();
171-
}
172-
}
173-
174-
/**
175-
* The global Python shell for the project.
176-
*
177-
* This shell instance will be maintained throughout the entire lifetime of a flow. Any variables,
178-
* functions, and objects which are created will be kept in memory until the flow ends.
179-
*/
180-
module.exports.PythonShell = new PythonShell();
181-
module.exports.PythonShellClass = PythonShell;
42+
createVirtualEnvironment: createVirtualEnvironment,
43+
};

nodes/quantum-algorithms/grovers/grovers.js

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const util = require('util');
44
const snippets = require('../../snippets');
55
const errors = require('../../errors');
66
const logger = require('../../logger');
7-
const {PythonShellClass} = require('../../python');
8-
const shell = new PythonShellClass();
7+
const {PythonInteractive, PythonPath} = require('../../python');
8+
const shell = new PythonInteractive(PythonPath);
99

1010
module.exports = function(RED) {
1111
function GroversNode(config) {
@@ -25,24 +25,25 @@ module.exports = function(RED) {
2525
return;
2626
}
2727

28-
const script = util.format(snippets.GROVERS, msg.payload);
29-
await shell.start();
30-
await shell.execute(script, (err, data) => {
31-
logger.trace(node.id, 'Executed grovers command');
32-
if (err) {
33-
logger.error(node.id, err);
34-
done(err);
35-
} else {
36-
data = data.split('\n');
37-
msg.payload = {
38-
topMeasurement: data[0],
39-
iterationsNum: Number(data[1]),
40-
};
41-
send(msg);
42-
done();
43-
}
44-
});
45-
shell.stop();
28+
let script = util.format(snippets.GROVERS, msg.payload);
29+
30+
shell.start();
31+
await shell.execute(script)
32+
.then((data) => {
33+
data = data.split('\n');
34+
msg.payload = {
35+
topMeasurement: data[0],
36+
iterationsNum: Number(data[1]),
37+
};
38+
send(msg);
39+
done();
40+
}).catch((err) => {
41+
logger.error(node.id, err);
42+
done(err);
43+
}).finally(() => {
44+
logger.trace(node.id, 'Executed grovers command');
45+
shell.stop();
46+
});
4647
});
4748
}
4849
RED.nodes.registerType('grovers', GroversNode);

nodes/quantum-algorithms/shors/shors.js

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const util = require('util');
44
const snippets = require('../../snippets');
55
const errors = require('../../errors');
66
const logger = require('../../logger');
7-
const {PythonShellClass} = require('../../python');
8-
const shell = new PythonShellClass();
7+
const {PythonInteractive, PythonPath} = require('../../python');
8+
const shell = new PythonInteractive(PythonPath);
99

1010
module.exports = function(RED) {
1111
function ShorsNode(config) {
@@ -36,33 +36,32 @@ module.exports = function(RED) {
3636
return;
3737
}
3838

39-
const params = Number(msg.payload);
40-
const script = util.format(snippets.SHORS, params);
41-
await shell.start();
42-
await shell.execute(script, (err, data) => {
43-
logger.trace(node.id, 'Executed shors command');
44-
if (err) {
45-
node.status({
46-
fill: 'red',
47-
shape: 'dot',
48-
text: 'Factorisation failed!',
49-
});
50-
logger.error(node.id, err);
51-
done(err);
52-
} else {
53-
node.status({
54-
fill: 'green',
55-
shape: 'dot',
56-
text: 'Factorisation completed!',
39+
let params = Number(msg.payload);
40+
let script = util.format(snippets.SHORS, params);
41+
42+
shell.start();
43+
await shell.execute(script)
44+
.then((data) => {
45+
node.status({
46+
fill: 'green',
47+
shape: 'dot',
48+
text: 'Factorisation completed!',
49+
});
50+
msg.payload = {listOfFactors: data};
51+
send(msg);
52+
done();
53+
}).catch((err) => {
54+
node.status({
55+
fill: 'red',
56+
shape: 'dot',
57+
text: 'Factorisation failed!',
58+
});
59+
logger.error(node.id, err);
60+
done(err);
61+
}).finally(() => {
62+
logger.trace(node.id, 'Executed shors command');
63+
shell.stop();
5764
});
58-
msg.payload = {
59-
listOfFactors: data,
60-
};
61-
send(msg);
62-
done();
63-
}
64-
});
65-
shell.stop();
6665
});
6766
}
6867
RED.nodes.registerType('shors', ShorsNode);

nodes/quantum/barrier/barrier.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,19 @@ module.exports = function(RED) {
7979

8080
// Run the script in the python shell, and if no error occurs
8181
// then send one qubit object per node output
82-
await shell.execute(script, (err) => {
83-
logger.trace(node.id, 'Executed barrier command');
84-
if (err) {
85-
logger.error(node.id, err);
86-
done(err);
87-
} else {
88-
send(node.qubits);
89-
done();
90-
}
91-
reset();
92-
});
82+
await shell.execute(script)
83+
.then(() => {
84+
send(node.qubits);
85+
done();
86+
})
87+
.catch((err) => {
88+
logger.error(node.id, err);
89+
done(err);
90+
})
91+
.finally(() => {
92+
logger.trace(node.id, 'Executed barrier command');
93+
reset();
94+
});
9395
}
9496
});
9597
}

0 commit comments

Comments
 (0)