UNPKG

15.3 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11Object.defineProperty(exports, "__esModule", { value: true });
12exports.PythonShell = exports.NewlineTransformer = exports.PythonShellError = void 0;
13const events_1 = require("events");
14const child_process_1 = require("child_process");
15const os_1 = require("os");
16const path_1 = require("path");
17const stream_1 = require("stream");
18const fs_1 = require("fs");
19const util_1 = require("util");
20function toArray(source) {
21 if (typeof source === 'undefined' || source === null) {
22 return [];
23 }
24 else if (!Array.isArray(source)) {
25 return [source];
26 }
27 return source;
28}
29/**
30 * adds arguments as properties to obj
31 */
32function extend(obj, ...args) {
33 Array.prototype.slice.call(arguments, 1).forEach(function (source) {
34 if (source) {
35 for (let key in source) {
36 obj[key] = source[key];
37 }
38 }
39 });
40 return obj;
41}
42/**
43 * gets a random int from 0-10000000000
44 */
45function getRandomInt() {
46 return Math.floor(Math.random() * 10000000000);
47}
48const execPromise = (0, util_1.promisify)(child_process_1.exec);
49class PythonShellError extends Error {
50}
51exports.PythonShellError = PythonShellError;
52/**
53 * Takes in a string stream and emits batches seperated by newlines
54 */
55class NewlineTransformer extends stream_1.Transform {
56 _transform(chunk, encoding, callback) {
57 let data = chunk.toString();
58 if (this._lastLineData)
59 data = this._lastLineData + data;
60 const lines = data.split(os_1.EOL);
61 this._lastLineData = lines.pop();
62 //@ts-ignore this works, node ignores the encoding if it's a number
63 lines.forEach(this.push.bind(this));
64 callback();
65 }
66 _flush(done) {
67 if (this._lastLineData)
68 this.push(this._lastLineData);
69 this._lastLineData = null;
70 done();
71 }
72}
73exports.NewlineTransformer = NewlineTransformer;
74/**
75 * An interactive Python shell exchanging data through stdio
76 * @param {string} script The python script to execute
77 * @param {object} [options] The launch options (also passed to child_process.spawn)
78 * @param [stdoutSplitter] Optional. Splits stdout into chunks, defaulting to splitting into newline-seperated lines
79 * @param [stderrSplitter] Optional. splits stderr into chunks, defaulting to splitting into newline-seperated lines
80 * @constructor
81 */
82class PythonShell extends events_1.EventEmitter {
83 /**
84 * spawns a python process
85 * @param scriptPath path to script. Relative to current directory or options.scriptFolder if specified
86 * @param options
87 * @param stdoutSplitter Optional. Splits stdout into chunks, defaulting to splitting into newline-seperated lines
88 * @param stderrSplitter Optional. splits stderr into chunks, defaulting to splitting into newline-seperated lines
89 */
90 constructor(scriptPath, options, stdoutSplitter = null, stderrSplitter = null) {
91 super();
92 /**
93 * returns either pythonshell func (if val string) or custom func (if val Function)
94 */
95 function resolve(type, val) {
96 if (typeof val === 'string') {
97 // use a built-in function using its name
98 return PythonShell[type][val];
99 }
100 else if (typeof val === 'function') {
101 // use a custom function
102 return val;
103 }
104 }
105 if (scriptPath.trim().length == 0)
106 throw Error("scriptPath cannot be empty! You must give a script for python to run");
107 let self = this;
108 let errorData = '';
109 events_1.EventEmitter.call(this);
110 options = extend({}, PythonShell.defaultOptions, options);
111 let pythonPath;
112 if (!options.pythonPath) {
113 pythonPath = PythonShell.defaultPythonPath;
114 }
115 else
116 pythonPath = options.pythonPath;
117 let pythonOptions = toArray(options.pythonOptions);
118 let scriptArgs = toArray(options.args);
119 this.scriptPath = (0, path_1.join)(options.scriptPath || '', scriptPath);
120 this.command = pythonOptions.concat(this.scriptPath, scriptArgs);
121 this.mode = options.mode || 'text';
122 this.formatter = resolve('format', options.formatter || this.mode);
123 this.parser = resolve('parse', options.parser || this.mode);
124 // We don't expect users to ever format stderr as JSON so we default to text mode
125 this.stderrParser = resolve('parse', options.stderrParser || 'text');
126 this.terminated = false;
127 this.childProcess = (0, child_process_1.spawn)(pythonPath, this.command, options);
128 ['stdout', 'stdin', 'stderr'].forEach(function (name) {
129 self[name] = self.childProcess[name];
130 self.parser && self[name] && self[name].setEncoding(options.encoding || 'utf8');
131 });
132 // Node buffers stdout&stderr in batches regardless of newline placement
133 // This is troublesome if you want to recieve distinct individual messages
134 // for example JSON parsing breaks if it recieves partial JSON
135 // so we use newlineTransformer to emit each batch seperated by newline
136 if (this.parser && this.stdout) {
137 if (!stdoutSplitter)
138 stdoutSplitter = new NewlineTransformer();
139 // note that setting the encoding turns the chunk into a string
140 stdoutSplitter.setEncoding(options.encoding || 'utf8');
141 this.stdout.pipe(stdoutSplitter).on('data', (chunk) => {
142 this.emit('message', self.parser(chunk));
143 });
144 }
145 // listen to stderr and emit errors for incoming data
146 if (this.stderrParser && this.stderr) {
147 if (!stderrSplitter)
148 stderrSplitter = new NewlineTransformer();
149 // note that setting the encoding turns the chunk into a string
150 stderrSplitter.setEncoding(options.encoding || 'utf8');
151 this.stderr.pipe(stderrSplitter).on('data', (chunk) => {
152 this.emit('stderr', self.stderrParser(chunk));
153 });
154 }
155 if (this.stderr) {
156 this.stderr.on('data', function (data) {
157 errorData += '' + data;
158 });
159 this.stderr.on('end', function () {
160 self.stderrHasEnded = true;
161 terminateIfNeeded();
162 });
163 }
164 else {
165 self.stderrHasEnded = true;
166 }
167 if (this.stdout) {
168 this.stdout.on('end', function () {
169 self.stdoutHasEnded = true;
170 terminateIfNeeded();
171 });
172 }
173 else {
174 self.stdoutHasEnded = true;
175 }
176 this.childProcess.on('error', function (err) {
177 self.emit('error', err);
178 });
179 this.childProcess.on('exit', function (code, signal) {
180 self.exitCode = code;
181 self.exitSignal = signal;
182 terminateIfNeeded();
183 });
184 function terminateIfNeeded() {
185 if (!self.stderrHasEnded || !self.stdoutHasEnded || (self.exitCode == null && self.exitSignal == null))
186 return;
187 let err;
188 if (self.exitCode && self.exitCode !== 0) {
189 if (errorData) {
190 err = self.parseError(errorData);
191 }
192 else {
193 err = new PythonShellError('process exited with code ' + self.exitCode);
194 }
195 err = extend(err, {
196 executable: pythonPath,
197 options: pythonOptions.length ? pythonOptions : null,
198 script: self.scriptPath,
199 args: scriptArgs.length ? scriptArgs : null,
200 exitCode: self.exitCode
201 });
202 // do not emit error if only a callback is used
203 if (self.listeners('pythonError').length || !self._endCallback) {
204 self.emit('pythonError', err);
205 }
206 }
207 self.terminated = true;
208 self.emit('close');
209 self._endCallback && self._endCallback(err, self.exitCode, self.exitSignal);
210 }
211 ;
212 }
213 /**
214 * checks syntax without executing code
215 * @returns rejects promise w/ string error output if syntax failure
216 */
217 static checkSyntax(code) {
218 return __awaiter(this, void 0, void 0, function* () {
219 const randomInt = getRandomInt();
220 const filePath = (0, os_1.tmpdir)() + path_1.sep + `pythonShellSyntaxCheck${randomInt}.py`;
221 const writeFilePromise = (0, util_1.promisify)(fs_1.writeFile);
222 return writeFilePromise(filePath, code).then(() => {
223 return this.checkSyntaxFile(filePath);
224 });
225 });
226 }
227 static getPythonPath() {
228 return this.defaultOptions.pythonPath ? this.defaultOptions.pythonPath : this.defaultPythonPath;
229 }
230 /**
231 * checks syntax without executing code
232 * @returns {Promise} rejects w/ stderr if syntax failure
233 */
234 static checkSyntaxFile(filePath) {
235 return __awaiter(this, void 0, void 0, function* () {
236 const pythonPath = this.getPythonPath();
237 let compileCommand = `${pythonPath} -m py_compile ${filePath}`;
238 return execPromise(compileCommand);
239 });
240 }
241 /**
242 * Runs a Python script and returns collected messages
243 * @param {string} scriptPath The path to the script to execute
244 * @param {Options} options The execution options
245 * @param {Function} callback The callback function to invoke with the script results
246 * @return {PythonShell} The PythonShell instance
247 */
248 static run(scriptPath, options, callback) {
249 let pyshell = new PythonShell(scriptPath, options);
250 let output = [];
251 return pyshell.on('message', function (message) {
252 output.push(message);
253 }).end(function (err) {
254 return callback(err ? err : null, output.length ? output : null);
255 });
256 }
257 ;
258 /**
259 * Runs the inputted string of python code and returns collected messages. DO NOT ALLOW UNTRUSTED USER INPUT HERE!
260 * @param {string} code The python code to execute
261 * @param {Options} options The execution options
262 * @param {Function} callback The callback function to invoke with the script results
263 * @return {PythonShell} The PythonShell instance
264 */
265 static runString(code, options, callback) {
266 // put code in temp file
267 const randomInt = getRandomInt();
268 const filePath = os_1.tmpdir + path_1.sep + `pythonShellFile${randomInt}.py`;
269 (0, fs_1.writeFileSync)(filePath, code);
270 return PythonShell.run(filePath, options, callback);
271 }
272 ;
273 static getVersion(pythonPath) {
274 if (!pythonPath)
275 pythonPath = this.getPythonPath();
276 return execPromise(pythonPath + " --version");
277 }
278 static getVersionSync(pythonPath) {
279 if (!pythonPath)
280 pythonPath = this.getPythonPath();
281 return (0, child_process_1.execSync)(pythonPath + " --version").toString();
282 }
283 /**
284 * Parses an error thrown from the Python process through stderr
285 * @param {string|Buffer} data The stderr contents to parse
286 * @return {Error} The parsed error with extended stack trace when traceback is available
287 */
288 parseError(data) {
289 let text = '' + data;
290 let error;
291 if (/^Traceback/.test(text)) {
292 // traceback data is available
293 let lines = text.trim().split(os_1.EOL);
294 let exception = lines.pop();
295 error = new PythonShellError(exception);
296 error.traceback = data;
297 // extend stack trace
298 error.stack += os_1.EOL + ' ----- Python Traceback -----' + os_1.EOL + ' ';
299 error.stack += lines.slice(1).join(os_1.EOL + ' ');
300 }
301 else {
302 // otherwise, create a simpler error with stderr contents
303 error = new PythonShellError(text);
304 }
305 return error;
306 }
307 ;
308 /**
309 * Sends a message to the Python shell through stdin
310 * Override this method to format data to be sent to the Python process
311 * @returns {PythonShell} The same instance for chaining calls
312 */
313 send(message) {
314 if (!this.stdin)
315 throw new Error("stdin not open for writing");
316 let data = this.formatter ? this.formatter(message) : message;
317 if (this.mode !== 'binary')
318 data += os_1.EOL;
319 this.stdin.write(data);
320 return this;
321 }
322 ;
323 /**
324 * Closes the stdin stream. Unless python is listening for stdin in a loop
325 * this should cause the process to finish its work and close.
326 * @returns {PythonShell} The same instance for chaining calls
327 */
328 end(callback) {
329 if (this.childProcess.stdin) {
330 this.childProcess.stdin.end();
331 }
332 this._endCallback = callback;
333 return this;
334 }
335 ;
336 /**
337 * Sends a kill signal to the process
338 * @returns {PythonShell} The same instance for chaining calls
339 */
340 kill(signal) {
341 this.terminated = this.childProcess.kill(signal);
342 return this;
343 }
344 ;
345 /**
346 * Alias for kill.
347 * @deprecated
348 */
349 terminate(signal) {
350 // todo: remove this next breaking release
351 return this.kill(signal);
352 }
353}
354exports.PythonShell = PythonShell;
355// starting 2020 python2 is deprecated so we choose 3 as default
356PythonShell.defaultPythonPath = process.platform != "win32" ? "python3" : "python";
357PythonShell.defaultOptions = {}; //allow global overrides for options
358// built-in formatters
359PythonShell.format = {
360 text: function toText(data) {
361 if (!data)
362 return '';
363 else if (typeof data !== 'string')
364 return data.toString();
365 return data;
366 },
367 json: function toJson(data) {
368 return JSON.stringify(data);
369 }
370};
371//built-in parsers
372PythonShell.parse = {
373 text: function asText(data) {
374 return data;
375 },
376 json: function asJson(data) {
377 return JSON.parse(data);
378 }
379};
380;
381//# sourceMappingURL=index.js.map
\No newline at end of file