├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── assets └── screenshot.png ├── frontend └── app.py ├── requirements.txt ├── scripts └── install_batavia ├── server ├── __init__.py ├── app.py └── utils.py ├── static └── stdouterr.js └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .mypy_cache 3 | venv/ 4 | static/batavia.js 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "batavia"] 2 | path = batavia 3 | url = git@github.com:beeware/batavia.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 florimondmanca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-in-browser 2 | 3 | Running Python in the browser using [Batavia] and [Starlette]. 4 | 5 | [batavia]: https://github.com/beeware/batavia 6 | [starlette]: https://www.starlette.io 7 | 8 | ## Demo 9 | 10 | ```python 11 | # frontend/app.py 12 | import dom 13 | 14 | print("Hello from Python!") 15 | 16 | dom.document.title = "Hello world" 17 | div = dom.document.getElementById("pyconsole") 18 | div.innerHTML = div.innerHTML + "\n\nHello, World!\n\n" 19 | 20 | raise ValueError("Just testing out exceptions!") 21 | ``` 22 | 23 | ![](assets/screenshot.png) 24 | 25 | ## Installation 26 | 27 | First, install Batavia. The full instructions are [here](https://batavia.readthedocs.io/en/latest/tutorial/tutorial-0.html), but we provide a convenience script that should work on Linux/macOS: 28 | 29 | ```bash 30 | chmod +x scripts/install_batavia 31 | ./scripts/install_batavia 32 | ``` 33 | 34 | Then install Python dependencies: 35 | 36 | ```bash 37 | python -m venv venv 38 | . venv/bin/activate 39 | pip install -r requirements.txt 40 | ``` 41 | 42 | You're good to go! 43 | 44 | ## Quickstart 45 | 46 | Start the web server: 47 | 48 | ```bash 49 | uvicorn server:app 50 | ``` 51 | 52 | Go to http://127.0.0.1:8000, and the following will happen: 53 | 54 | - An HTML page is shown. It was server-rendered via Jinja2 by the Starlette web app. 55 | - The HTML page contains a script that contains the _Python bytecode_ for `frontend/app.py`. 56 | - The Batavia JS virtual machine is loaded and _runs that bytecode_. 57 | - As a result, `frontend/app.py` ends up _running in your browser_, and doing stuff such as printing to the browser console and manipulating the DOM. 58 | 59 | ✨🌟✨ 60 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florimondmanca/python-in-browser/7cafaadb7151b81f1b0d39cf4013f30e3d150393/assets/screenshot.png -------------------------------------------------------------------------------- /frontend/app.py: -------------------------------------------------------------------------------- 1 | import dom 2 | 3 | print("Hello from Python!") 4 | 5 | dom.document.title = "Hello world" 6 | div = dom.document.getElementById("pyconsole") 7 | div.innerHTML = div.innerHTML + "\n\nHello, World!\n\n" 8 | 9 | raise ValueError("Just testing out exceptions!") 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # App 2 | starlette 3 | aiofiles 4 | jinja2 5 | 6 | # Server 7 | uvicorn 8 | 9 | # Tooling 10 | pylint 11 | black 12 | mypy 13 | -------------------------------------------------------------------------------- /scripts/install_batavia: -------------------------------------------------------------------------------- 1 | #! /bin/sh -e 2 | 3 | cd batavia 4 | npm install 5 | npm run build 6 | cd .. 7 | cp batavia/dist/batavia.js static/ 8 | -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import app 2 | -------------------------------------------------------------------------------- /server/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from starlette.applications import Starlette 4 | from starlette.requests import Request 5 | from starlette.responses import Response 6 | from starlette.templating import Jinja2Templates 7 | from starlette.staticfiles import StaticFiles 8 | 9 | from .utils import bytecode 10 | 11 | app = Starlette(debug=True) 12 | app.mount("/static", StaticFiles(directory="static"), name="static") 13 | 14 | templates = Jinja2Templates(directory="templates") 15 | 16 | 17 | @app.route("/") 18 | async def homepage(request: Request) -> Response: 19 | context = {"request": request, "code": bytecode(os.path.join("frontend", "app.py"))} 20 | return templates.TemplateResponse("index.html", context) 21 | -------------------------------------------------------------------------------- /server/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import py_compile 4 | import tempfile 5 | 6 | 7 | def bytecode(sourcefile: str) -> dict: 8 | fd, tempname = tempfile.mkstemp() 9 | # Immediately close the file so that we can write/move it etc implicitly 10 | # below without nasty permission errors 11 | os.close(fd) 12 | 13 | py_compile.compile(sourcefile, cfile=tempname, doraise=True) 14 | try: 15 | with open( 16 | os.path.join(os.path.dirname(sourcefile), tempname), "rb" 17 | ) as compiled: 18 | payload = base64.encodebytes(compiled.read()).decode() 19 | return {"compiled": payload, "filename": sourcefile} 20 | finally: 21 | if os.path.exists(tempname): 22 | os.remove(tempname) 23 | -------------------------------------------------------------------------------- /static/stdouterr.js: -------------------------------------------------------------------------------- 1 | // Define a stdout and stderr function that will output to the page. 2 | 3 | class PyConsole { 4 | write(buffer) { 5 | if (buffer === "\n") return; 6 | console.log(buffer); 7 | } 8 | flush() {} 9 | } 10 | 11 | class PyErrConsole { 12 | write(buffers) { 13 | console.error(buffers[0]); 14 | } 15 | flush() {} 16 | } 17 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Batavia + Starlette 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 37 | 38 | 39 | 40 |

Batavia + Starlette

41 |

Open your console!

42 | 43 |

44 |   
45 | 
46 | 


--------------------------------------------------------------------------------