Python#
The {exec} python
directive allows executing
Python code in the browser.
import platform
print(f"{platform.python_implementation()}"
f" {'.'.join(platform.python_version_tuple())}"
f" on {platform.platform()}")
Module setup#
Module setup code can be defined as a named
{exec} python
block, to be referenced in the
:after:
option of other blocks.
1def factorial(n):
2 res = 1
3 for i in range(2, n + 1):
4 res *= i
5 return res
Program output#
Terminal#
The terminal output generated by an
{exec} python
block via sys.stdout
and sys.stderr
(and therefore, via print()
) is displayed in
an output block. Output to sys.stderr
is colored.
for i in range(10):
print(f"factorial({i}) = {factorial(i)}")
import sys
sys.stderr.write("Program terminated.\n")
The terminal output block can be cleared with the form-feed control character
(\x0c
).
import asyncio
for i in range(10, 0, -1):
print(f"\x0c{i}...")
await asyncio.sleep(1)
print("\x0cHappy new year!")
The width of the terminal output block is limited by the page content width.
print("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
"minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip "
"ex ea commodo consequat. Duis aute irure dolor in reprehenderit in "
"voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur "
"sint occaecat cupidatat non proident, sunt in culpa qui officia "
"deserunt mollit anim id est laborum.")
The height of the terminal output block can be limited with
:output-style:
.
for i in range(100):
print(i)
Graphics#
See the tdoc.svg
module.
from tdoc import svg
def paint_heart(c):
c.path('M -40,-20 A 20,20 0,0,1 0,-20 A 20,20 0,0,1 40,-20 '
'Q 40,10 0,40 Q -40,10 -40,-20 z',
stroke='red', fill='transparent')
c.path('M -40,30 -30,30 -30,40 '
'M -30,30 0,0 M 34,-34 45,-45'
'M 35,-45 45,-45 45,-35',
stroke=svg.Stroke('black', width=2), fill='transparent')
img = svg.Image(400, 100, stroke='darkorange', fill='#c0c0ff',
style='width: 100%; height: 100%')
img.stylesheet = """
.bold {
stroke: blue;
stroke-width: 2;
fill: #c0ffc0;
}
"""
img.circle(20, 30, 10)
img.ellipse(20, 70, 10, 20, klass='bold')
img.line(0, 0, 400, 100)
g = img.group(transform=svg.translate(200, 10))
g.polygon((0, 0), (30, 0), (40, 20), klass='bold')
g.polyline((0, 0), (30, 0), (40, 20), fill='transparent',
transform=svg.translate(x=50, y=10))
img.rect(0, 0, 400, 100, fill='transparent')
img.text(50, 90, "Some text", stroke='transparent', fill='green')
paint_heart(img.group(transform=svg.translate(360, 30).rotate(20).scale(0.5)))
render(img)
Animations can be implemented by rendering images repeatedly in a loop, with a short sleep between images. Don't forget to sleep, otherwise the program becomes unstoppable and the page must be reloaded.
import asyncio
import random
img = svg.Image(400, 100, style='width: 100%; height: 100%')
sym = img.symbol()
paint_heart(sym)
hearts = [(img.use(href=sym),
random.uniform(0, 100), random.uniform(0, 100),
random.uniform(-180, 180))
for _ in range(20)]
def saw(value, amplitude):
return abs((value + amplitude) % (2 * amplitude) - amplitude)
def pose(t, vx, vy, va):
return saw(t * vx, img.width), saw(t * vy, img.height), (t * va) % 360.0
loop = asyncio.get_running_loop()
start = loop.time()
while True:
t = loop.time() - start
for heart, vx, vy, va in hearts:
heart.x, heart.y, a = pose(t, vx, vy, va)
heart.transform = svg.rotate(a, heart.x, heart.y)
img.width, img.height = await render(img)
await asyncio.sleep(1 / 60)
Program input#
User input can be requested by await
ing functions available in the global
environment. Unfortunately, sys.stdin
(and anything that depends on
it) cannot be used, due to its blocking nature.
Line of text#
See input_line()
.
name = await input_line("What is your name?")
print(f"Hello, {name}!")
Multi-line text#
See input_text()
.
print("Please enter some text.")
text = await input_text()
print(f"\x0cThe text was:\n-------------\n{text}")
Pause#
See pause()
.
n = 5
fact = 1
for i in range(2, n + 1):
fact *= i
await pause(f"i={i}, fact={fact}")
print(f"The factorial of {n} is {fact}")
Exceptions#
Exceptions that propagate out of the
{exec} python
block are displayed as a
traceback on sys.stderr
.
def outer():
try:
inner()
except Exception as e:
raise Exception("inner() failed") from e
def inner():
raise Exception("Something is broken")
outer()
Concurrency#
All {exec} python
blocks on a page are executed
in a shared, single-threaded interpreter. Therefore, only one block can run at
any given time. Nevertheless, concurrent execution is possible through async
coroutines. The asyncio
module provides a lot of functionality related
to async
concurrency.
import asyncio
import time
while True:
print(f"\x0c{time.strftime('%Y-%m-%d %H:%M:%S')}")
await asyncio.sleep(1)
import asyncio
i = 0
while True:
print(f"\x0ci={i}")
i += 1
await asyncio.sleep(0.2)
Call async
functions synchronously#
It is possible to invoke async
functions synchronously with
pyodide.ffi.run_sync()
. This requires the experimental
WebAssembly JavaScript promise integration API
(JSPI), which isn't widely supported yet.
Google Chrome: The serving domain must be registered for the origin trial. For local testing, the feature can be enabled with a flag (
chrome://flags/#enable-experimental-webassembly-jspi
).Microsoft Edge: The serving domain must be registered for the origin trial. For local testing, the feature can be enabled with a flag (
edge://flags/#enable-experimental-webassembly-jspi
).Firefox: Firefox doesn't have an origin trial yet, and is still working on the implementation. For local testing, the feature can be enabled with a config (
javascript.options.wasm_js_promise_integration
), but it doesn't work with Pyodide yet.Safari: Safari doesn't currently support JSPI.
from pyodide import ffi
def input(prompt):
return ffi.run_sync(input_line(prompt))
name = input("Name:")
print(f"Hello, {name}!")
Packages#
Additional packages can be made available through the exec:python:packages
metadata, which holds a
list of packages
to load.
import pathlib
import sqlite3
path = pathlib.Path('database.sqlite')
exists = path.exists()
db = sqlite3.connect(path)
if not exists:
print("Creating database")
db.executescript(pathlib.Path('database.sql').read_text())
for k, v in db.execute('select * from kv;'):
print(f"key: {k}, value: {v}")
Filesystem#
The block below lists all the files and directories on the virtual filesystem seen by Python code.
import pathlib
paths = []
for base, dirs, files in pathlib.Path('/').walk(on_error=lambda e: None):
if base == pathlib.Path('/proc/self'): dirs.remove('fd')
paths.extend(str(base / e) for e in dirs + files)
paths.sort()
print('\n'.join(paths))