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:
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.