|
2 | 2 |
|
3 | 3 | const os = require('os');
|
4 | 4 | const path = require('path');
|
5 |
| -const fileSystem = require('fs'); |
| 5 | +const util = require('util'); |
| 6 | +const exec = util.promisify(require('child_process').exec); |
6 | 7 | const pythonExecutable = os.platform() === 'win32' ? 'venv/Scripts/python.exe' : 'venv/bin/python';
|
7 | 8 | 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'); |
12 | 10 |
|
13 | 11 |
|
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}`); |
61 | 15 | }
|
62 | 16 |
|
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 |
| - } |
76 | 17 |
|
| 18 | +module.exports = { |
77 | 19 | /**
|
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. |
81 | 21 | *
|
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. |
93 | 23 | */
|
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, |
115 | 25 |
|
116 | 26 | /**
|
117 |
| - * Spawns a new Python process. |
| 27 | + * The global Python shell instance. |
118 | 28 | *
|
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. |
125 | 31 | */
|
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), |
139 | 33 |
|
140 | 34 | /**
|
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. |
145 | 36 | */
|
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, |
157 | 38 |
|
158 | 39 | /**
|
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. |
167 | 41 | */
|
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 | +}; |
0 commit comments