Node.js: Is it possible to make an adaptor for python interactive shell (REPL)?

Viewed 237

It is possible to use readline in Node.js to read a line of python code, then send it to a python interactive shell spawned as a child process, then receive the output?

I know this is possible:

import { spawn } from "child_process";

const py = spawn("python", ["-i"]);

py.stdout.on("data", (data) => {
  console.log(`stdout: ${data}`);
});

py.stdin.write('print("hello from python!")\n');

// will get 'stdout: hello from python!'

However, this will not work:

import { spawn } from "child_process";
import * as readline from "readline";

const py = spawn("python", ["-i"]);

py.stdout.on("data", (data) => {
  console.log(`stdout: ${data}`);
});

py.stdin.write('print("hello from python!")\n');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});
rl.prompt();
rl.on("line", (line) => {
  py.stdin.write(line);
  rl.prompt();
}).on("close", () => {
  process.exit()
});

I want to do this because I want to develop a interface for python in Node.js. Executing a standalone python command/file is simple via spawn, but I want to build a fully-featured interface, which will have similar capabilities as Reticulate in R.

1 Answers

I have a similar application and I managed to get it running enough for my needs: a processToPromise function. The drawback is that it always has to register new event listeners on each call. Basically, it waits for the interactive Python shell to print it's >>> after your command has finished and gives you the output that came in the meantime.

Also, you cannot call this function multiple times asynchronously without getting messed up results. Try some simple print statements first, to see that you get what you expect.


Some code for reference:

const child_process = require('child_process');

class InteractiveProcessHandle {
    private process: any;

    public outputLog: string;
    public latestOutput: string;

    private processToPromise(process: any) {
        return new Promise<string>((resolve, reject) => {
            console.log("+++ creating promise");
            process.stdout.removeAllListeners();
            process.stderr.removeAllListeners();
            process.stdin.removeAllListeners();

            let lastString: string = '';

            process.stdout.on("data", (data: any) => {
                data = data.toString().trim();
                this.update(data);
                if (data.endsWith('>>>')) {
                    resolve(lastString);
                }
                lastString = data;
            });
            process.stderr.on("data", (data: any) => {
                data = data.toString().trim();
                this.update(data);
                if (data.endsWith('>>>')) {
                    resolve(lastString);
                }
                lastString = data;
            });
            process.stdin.on("error", () => {
                console.log("Failure in stdin! ... error");
                reject();
            });
            process.stdin.on("close", () => {
                console.log("Failure in stdin! ... close");
                reject();
            });
            process.stdin.on("end", () => {
                console.log("Failure in stdin! ... end");
                reject();
            });
            process.stdin.on("disconnect", () => {
                console.log("Failure in stdin! ... disconnect");
                reject();
            });
            process.stdout.on("error", () => {
                console.log("Failure in stdout! ... error");
                reject();
            });
            process.stdout.on("close", () => {
                console.log("Failure in stdout! ... close");
                reject();
            });
            process.stdout.on("end", () => {
                console.log("Failure in stdout! ... end");
                reject();
            });
            process.stderr.on("error", () => {
                console.log("Failure in stderr! ... error");
                reject();
            });
            process.stderr.on("close", () => {
                console.log("Failure in stderr! ... close");
                reject();
            });
            process.stderr.on("end", () => {
                console.log("Failure in stderr! ... end");
                reject();
            });
            console.log("+++ done creating promise");
        });
    }

    private update(data: any) {
        this.latestOutput = data;
        this.outputLog += data + "\n";
        console.log(`logging from update: "${data}"`);
    }

    public call(command: string): Promise<string> {
        console.log(`called: "${command.trim()}"`);
        let promise = this.processToPromise(this.process);
        this.process.stdin.write(command);
        return promise;
    }

    constructor(call: any, options: any) {
        this.latestOutput = "";
        this.outputLog = "";

        this.process = child_process.spawn(call, options, { shell: true });
        this.process.stdout.setEncoding('utf8');
        this.process.stderr.setEncoding('utf8');
    }
};

var interactive_python = new InteractiveProcessHandle('python', ['-i']);

async () => {
    await interactive_python .call('');
    await interactive_python .call('x = 20');
    let x = await clang_build.call('print(x)\n');
    console.log('x = ', x);
}
Related