UNPKG

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