Electron Adventures: Episode 57: Notebook Python HTTP Backend

Let's try to do the same thing in Python as we did in Ruby.

Frontend changes

We can reuse the frontend we just did. The only changes are different code examples in src/App.js:

  let [notebook, updateNotebook] = useImmer([
    { input: "def fib(n):\n  if n < 2:\n    return 1\n  return fib(n-1) + fib(n-2)", output: "" },
    { input: "print([fib(n) for n in range(1,11)])", output: "" },
    { input: "print(3**100)')", output: "" },
  ])

And different proxy address in package.json, as flask default is different to sinatra default:

  "proxy": "http://localhost:5000"

Python Language Server

The server will have same API to the Ruby one, with a single POST /code endpoint.

#!/usr/bin/env python3

from flask import Flask, request
from io import StringIO
import sys

class Capturing(list):
    def __enter__(self):
        self._stdout = sys.stdout
        self._stderr = sys.stderr
        self._stringio = StringIO()
        sys.stdout = self._stringio
        sys.stderr = self._stringio
        return self
    def __exit__(self, *args):
        output = self._stringio.getvalue()
        self.append(output)
        sys.stdout = self._stdout
        sys.stderr = self._stderr

app = Flask(__name__)

sessions = {}

@app.route("/code", methods=["POST"])
def code():
    body = request.json
    session_id = body["session_id"]
    code = body["code"]
    sessions.setdefault(session_id, {})
    error = None
    with Capturing() as output:
        try:
            exec(code, sessions[session_id])
        except Exception as e:
            error = str(e)

    return {"output": output[0], "error": error}

There are two things of note here.

First, Python doesn't have blocks, but it has a few close-enough equivalents for some use cases. Capturing uses StringIO to capture the output. As Python strings are not modifiable, and with cannot be used to pass any interesting object, we need to wrap the return value in a one element list. That's why we have to extract it with output[0], not just output. It would be cleaner with blocks but it's good enough.

And second, Python exec is a bit problematic. In principle it takes three arguments - the code to be executed, globals dictionary, and locals dictionary. Unfortunately if you use it this way, you cannot execute recursive functions. Python would set fib in locals dictionary, then when it tries to recurse, it would only look inside the globals dictionary. The only workaround is to pass same dictionary as both globals and locals, which conveniently is what already happens if we skip the last argument.

The rest of the code is just a few imports, getting data from JSON request, and a dictionary of sessions.

Running the app

To install the requirements you'll need:

$ pip3 install flask
$ npm install

Then run these in 3 terminals:

$ flask run
$ npm run start
$ npx electron .

Security

And just a reminder, this is literally an HTTP server which by design will execute any code anyone from the same machine sends it, so it's extremely insecure.

Result

Here's the result if we press "Run All" button:

electron-adventures-57-screenshot.png

In the next episode we'll try a different approach to communicating with the external programs.

As usual, all the code for the episode is here.