Electron Adventures: Episode 57: Notebook Python HTTP Backend
Tags: javascript, electron, react, python
Published: 2021-09-19 00:24:44

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

```javascript
  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:

```javascript
  "proxy": "http://localhost:5000"
```

### Python Language Server

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

```python
#!/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:

```shell
$ pip3 install flask
$ npm install
```

Then run these in 3 terminals:

```shell
$ 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:

![Episode 57 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/20ks8hbcliun8b3pnkf7.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](https://github.com/taw/electron-adventures/tree/master/episode-57-notebook-python-http-backend).