% Copyright 2024 Remy Blank <remy@c-space.org>
% SPDX-License-Identifier: MIT

# Python

```{metadata}
exec:
  python:
    files:
      database.sql:
    packages: [sqlite3]
```

The [`{exec} python`](../reference/exec.md#python) directive allows executing
Python code in the browser.

```{exec} python
:when: load
:class: hidden
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`](../reference/exec.md#python) block, to be referenced in the
{rst:dir}`:after: <exec:after>` option of other blocks.

```{exec} python
:name: python-setup
:when: never
:linenos:
def factorial(n):
  res = 1
  for i in range(2, n + 1):
    res *= i
  return res
```

```{defaults} exec
:when: load
:editor:
```

## Program output

### Terminal

The terminal output generated by an
[`{exec} python`](../reference/exec.md#python) block via {py:obj}`sys.stdout`
and {py:obj}`sys.stderr` (and therefore, via {py:func}`print`) is displayed in
an output block. Output to {py:obj}`sys.stderr` is colored.

```{exec} python
:after: python-setup
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`).

```{exec} python
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.

```{exec} python
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
{rst:dir}`:output-style: <exec:output-style>`.

```{exec} python
:output-style: max-height: 10rem
for i in range(100):
  print(i)
```

### Graphics

See the {py:mod}`tdoc.svg` module.

```{exec} python
:name: python-graphics
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.

```{exec} python
:after: python-graphics
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, {py:obj}`sys.stdin` (and anything that depends on
it) cannot be used, due to its blocking nature.

### Line of text

See {py:func}`~tdoc.core.input_line`.

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

### Multi-line text

See {py:func}`~tdoc.core.input_text`.

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

### Buttons

See {py:func}`~tdoc.core.input_buttons`.

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

### Pause

See {py:func}`~tdoc.core.pause`.

```{exec} python

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`](../reference/exec.md#python) block are displayed as a
traceback on {py:obj}`sys.stderr`.

```{exec} python
:after: python-setup
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`](../reference/exec.md#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 {py:mod}`asyncio` module provides a lot of functionality related
to `async` concurrency.

```{exec} python
import asyncio
import time

while True:
  print(f"\x0c{time.strftime('%Y-%m-%d %H:%M:%S')}")
  await asyncio.sleep(1)
```

```{exec} python
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](https://github.com/WebAssembly/js-promise-integration)
(JSPI), which isn't widely supported yet.

- **Chromium-based browsers (Google Chrome, Microsoft Edge):** The serving
  domain must be registered for the
  [origin trial](https://developer.chrome.com/origintrials/#/register_trial/1603844417297317889).
  For local testing, the feature can be enabled with a flag
  (`chrome://flags/#enable-experimental-webassembly-jspi`).
- **Firefox:** Firefox doesn't have an
  [origin trial](https://wiki.mozilla.org/Origin_Trials) yet, and is still
  working on the [implementation](https://bugzilla.mozilla.org/show_bug.cgi?id=1897981).
  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.

```{exec} python
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](https://docs.pyscript.net/latest/user-guide/configuration/#packages)
to load.

```{exec} python
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.

```{exec} python
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))
```
