Python#

The {exec} python directive allows executing Python code in the browser.

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.

1for i in range(10):
2  print(f"factorial({i}) = {factorial(i)}")
3
4import sys
5sys.stderr.write("Program terminated.\n")

The terminal output block can be cleared with the form-feed control character (\x0c).

1import asyncio
2
3for i in range(10, 0, -1):
4  print(f"\x0c{i}...")
5  await asyncio.sleep(1)
6print("\x0cHappy new year!")

The width of the terminal output block is limited by the page content width.

1print("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
2      "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
3      "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip "
4      "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in "
5      "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur "
6      "sint occaecat cupidatat non proident, sunt in culpa qui officia "
7      "deserunt mollit anim id est laborum.")

The height of the terminal output block can be limited with :console-style:.

1for i in range(100):
2  print(i)

Graphics#

See the tdoc.svg module.

 1from tdoc import svg
 2
 3def paint_heart(c):
 4  c.path('M -40,-20 A 20,20 0,0,1 0,-20 A 20,20 0,0,1 40,-20 '
 5         'Q 40,10 0,40 Q -40,10 -40,-20 z',
 6         stroke='red', fill='transparent')
 7  c.path('M -40,30 -30,30 -30,40 '
 8         'M -30,30 0,0 M 34,-34 45,-45'
 9         'M 35,-45 45,-45 45,-35',
10         stroke=svg.Stroke('black', width=2), fill='transparent')
11
12img = svg.Image(400, 100, stroke='darkorange', fill='#c0c0ff',
13                style='width: 100%; height: 100%;')
14img.stylesheet = """
15.bold {
16  stroke: blue;
17  stroke-width: 2;
18  fill: #c0ffc0;
19}
20"""
21img.circle(20, 30, 10)
22img.ellipse(20, 70, 10, 20, klass='bold')
23img.line(0, 0, 400, 100)
24g = img.group(transform=svg.translate(200, 10))
25g.polygon((0, 0), (30, 0), (40, 20), klass='bold')
26g.polyline((0, 0), (30, 0), (40, 20), fill='transparent',
27           transform=svg.translate(x=50, y=10))
28img.rect(0, 0, 400, 100, fill='transparent')
29img.text(50, 90, "Some text", stroke='transparent', fill='green')
30paint_heart(img.group(transform=svg.translate(360, 30).rotate(20).scale(0.5)))
31render(img)

Try holding the Ctrl key and moving the pointer over the image to view image coordinates. Then hover over the coordinates for instructions on how to paste them into an editor.

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.

 1import random
 2
 3img = svg.Image(400, 100, style='width: 100%; height: 100%;')
 4sym = img.symbol()
 5paint_heart(sym)
 6hearts = [(img.use(href=sym),
 7           random.uniform(0, 100), random.uniform(0, 100),
 8           random.uniform(-180, 180))
 9          for _ in range(20)]
10
11def saw(value, amplitude):
12  return abs((value + amplitude) % (2 * amplitude) - amplitude)
13
14def pose(t, vx, vy, va):
15  return saw(t * vx, img.width), saw(t * vy, img.height), (t * va) % 360.0
16
17start = await animation_frame()
18while True:
19  t = (await animation_frame() - start) / 1000
20  for heart, vx, vy, va in hearts:
21    heart.x, heart.y, a = pose(t, vx, vy, va)
22    heart.transform = svg.rotate(a, heart.x, heart.y)
23  img.width, img.height = await render(img)

Program input#

User input can be requested by awaiting 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().

1name = await input_line("What is your name?")
2print(f"Hello, {name}!")

Multi-line text#

See input_text().

1print("Please enter some text.")
2text = await input_text()
3print(f"\x0cThe text was:\n-------------\n{text}")

Buttons#

See input_buttons().

1colors = ["Red", "Green", "Blue"]
2index = await input_buttons("Pick a color:", colors)
3print(f"You picked: {colors[index]}")

Pause#

See pause().

1n = 5
2fact = 1
3for i in range(2, n + 1):
4  fact *= i
5  await pause(f"i={i}, fact={fact}")
6print(f"The factorial of {n} is {fact}")

Exceptions#

Uncaught exceptions are displayed as a traceback on sys.stderr.

 1def outer():
 2  try:
 3    inner()
 4  except Exception as e:
 5    raise Exception("inner() failed") from e
 6
 7def inner():
 8  raise Exception("Something is broken")
 9
10outer()

Friendly#

The following code block shows how to install the friendly package to improve the tracebacks of uncaught exceptions. It can be added to a page (hidden with :class: hidden) and used by other blocks on the page via an :after: dependency.

if once('friendly'):  # Install only once per interpreter
  # Install friendly and markdown-it-py from PyPI. The latter is required by
  # rich, which is used by friendly, but it isn't part of rich's dependencies.
  import micropip
  await micropip.install(['friendly', 'markdown-it-py'])
  # BUG(friendly-0.7.21): Importing friendly triggers a DeprecationWarning. It
  # also enables all warnings, so it's not possible to ignore the warning using
  # the standard warnings module. The next version should have a fix, but in the
  # meantime, add a filter to friendly_traceback.
  import friendly_traceback
  friendly_traceback.add_ignored_warnings(
    lambda m, w, *_: w is DeprecationWarning)
  # Activate friendly in French.
  import friendly
  friendly.install(lang='fr')
  # Exclude the tdoc.core module from tracebacks. This should normally use
  # friendly.exclude_file_from_traceback(), but the latter checks if the file
  # exists, and the check fails because the file is in a .zip archive.
  from tdoc import core
  from friendly_traceback.path_info import EXCLUDED_FILE_PATH
  EXCLUDED_FILE_PATH.add(core.__file__)

The following block uses the block above and raises an exception. It runs in a separate interpreter to avoid polluting the other blocks on this page; if all blocks on a page should use friendly, this isn't necessary.

1print("Importing foo...")
2import foo

Concurrency#

All {exec} python blocks on a page and referencing the same environment 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 functionality related to async concurrency.

1import asyncio
2import time
3
4while True:
5  print(f"\x0c{time.strftime('%Y-%m-%d %H:%M:%S')}")
6  await asyncio.sleep(1)
1import asyncio
2
3i = 0
4while True:
5  print(f"\x0ci={i}")
6  i += 1
7  await asyncio.sleep(0.2)

Calling async functions synchronously via pyodide.ffi.run_sync() only works on Chromium-based browsers.

1from pyodide import ffi
2
3def input(prompt):
4  return ffi.run_sync(input_line(prompt))
5
6name = input("Name:")
7print(f"Hello, {name}!")

SQLite#

The sqlite3 module is available and can be used to operate on SQLite databases. These databases are held in the interpreter's filesystem, and while they persist across program runs, they don't persist across page reloads.

 1import pathlib
 2import sqlite3
 3
 4path = pathlib.Path('database.sqlite')
 5exists = path.exists()
 6db = sqlite3.connect(path)
 7if not exists:
 8  print("Creating database")
 9  db.executescript(pathlib.Path('database.sql').read_text())
10for k, v in db.execute('select * from kv;'):
11  print(f"key: {k}, value: {v}")

Packages#

Additional packages can be made available through the exec.python.packages metadata, which holds a list of packages to load.

1import numpy as np
2
3a = np.array([1, 2, 3])
4b = np.array([4, 5, 6])
5print(a + b, a * b)

Packages can also be installed directly from PyPI using micropip (which must itself be added to the exec.python.packages metadata). For example, the following code installs the snowballstemmer package. Note how the installation is only performed once per interpreter, using once().

 1if once('install-snowballstemmer'):  # Install only once per interpreter
 2  import micropip
 3  await micropip.install(['snowballstemmer'])
 4
 5import snowballstemmer
 6stemmer = snowballstemmer.stemmer('english')
 7for word in ['running', 'runs', 'ran',
 8             'caring', 'cared', 'careful',
 9             'university', 'universities',
10             'fairly', 'unfairly',
11             'singing', 'singer', 'song']:
12  print(f"{word:12} => {stemmer.stemWord(word)}")

Filesystem#

The block below lists all the files and directories on the virtual filesystem seen by Python code.

1import pathlib
2
3paths = []
4for base, dirs, files in pathlib.Path('/').walk(on_error=lambda e: None):
5  if base == pathlib.Path('/proc/self'): dirs.remove('fd')
6  paths.extend(str(base / e) for e in dirs + files)
7paths.sort()
8print('\n'.join(paths))