Python#

The {exec} python block allows executing Python code directly 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.

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!")

Graphics#

The tdoc.svg module allows creating SVG images using simple drawing primitives. Rendering an image creates an output block displaying the image.

render(image, name='') -> Future

  • image: The image to be rendered.

  • name: The name of the output block. If a block with the same name already exists, it is replaced. Otherwise, a new block is added, keeping blocks ordered by name.

  • The returned future resolves to a tuple (width, height) that specifies the size of the rendered image.

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.styles = """
.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", 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=f'#{sym.id}'),
           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 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#

await input_line(prompt=None)

  • prompt: The text to display before the input field.

  • Returns the content of the input field as a str.

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

Multi-line text#

await input_text(prompt=None)

  • prompt: The text to display before the input field.

  • Returns the content of the input field as a str.

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

Buttons#

await input_buttons(prompt, labels)

  • prompt: The text to display before the buttons, or None to not display a prompt.

  • labels: The labels of the buttons, as a list.

  • Returns the index of the button that was clicked, as an int.

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

Pause#

await pause(prompt=None, label="@icon{forward-step}")

  • prompt: The text to display before the button, or None to not display a prompt.

  • label: The label of the button. If the label has the format @icon{...}, the corresponding icon from Font Awesome is used.

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)