diff --git a/docs/api.md b/docs/api.md
deleted file mode 100644
index 21327b0..0000000
--- a/docs/api.md
+++ /dev/null
@@ -1,1287 +0,0 @@
-# Built-in APIs
-
-PyScript makes available convenience objects, functions and attributes.
-
-In Python this is done via the builtin `pyscript` module:
-
-```python title="Accessing the document object via the pyscript module"
-from pyscript import document
-```
-
-In HTML this is done via `py-*` and `mpy-*` attributes (depending on the
-interpreter you're using):
-
-```html title="An example of a py-click handler"
-Click me
-```
-
-These APIs will work with both Pyodide and Micropython in exactly the same way.
-
-!!! info
-
- Both Pyodide and MicroPython provide access to two further lower-level
- APIs:
-
- * Access to
- [JavaScript's `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis)
- via importing the `js` module: `import js` (now `js` is a proxy for
- `globalThis` in which all native JavaScript based browser APIs are
- found).
- * Access to interpreter specific versions of utilities and the foreign
- function interface. Since these are different for each interpreter, and
- beyond the scope of PyScript's own documentation, please check each
- project's documentation
- ([Pyodide](https://pyodide.org/en/stable/usage/api-reference.html) /
- [MicroPython](https://docs.micropython.org/en/latest/)) for details of
- these lower-level APIs.
-
-PyScript can run in two contexts: the main browser thread, or on a web worker.
-The following three categories of API functionality explain features that are
-common for both main thread and worker, main thread only, and worker only. Most
-features work in both contexts in exactly the same manner, but please be aware
-that some are specific to either the main thread or a worker context.
-
-## Common features
-
-These Python objects / functions are available in both the main thread and in
-code running on a web worker:
-
-### `pyscript.config`
-
-A Python dictionary representing the configuration for the interpreter.
-
-```python title="Reading the current configuration."
-from pyscript import config
-
-
-# It's just a dict.
-print(config.get("files"))
-# This will be either "mpy" or "py" depending on the current interpreter.
-print(config["type"])
-```
-
-!!! info
-
- The `config` object will always include a `type` attribute set to either
- `mpy` or `py`, to indicate which version of Python your code is currently
- running in.
-
-!!! warning
-
- Changing the `config` dictionary at runtime has no effect on the actual
- configuration.
-
- It's just a convenience to **read the configuration** at run time.
-
-### `pyscript.current_target`
-
-A utility function to retrieve the unique identifier of the element used
-to display content. If the element is not a `
-```
-
-!!! Note
-
- The return value of `current_target()` always references a visible element
- on the page, **not** at the current `
- ```
-
- Then use the standard `document.getElementById(script_id)` function to
- return a reference to it in your code.
-
-### `pyscript.display`
-
-A function used to display content. The function is intelligent enough to
-introspect the object[s] it is passed and work out how to correctly display the
-object[s] in the web page based on the following mime types:
-
-* `text/plain` to show the content as text
-* `text/html` to show the content as *HTML*
-* `image/png` to show the content as ` `
-* `image/jpeg` to show the content as ` `
-* `image/svg+xml` to show the content as ``
-* `application/json` to show the content as *JSON*
-* `application/javascript` to put the content in `
- PyScript
--->
-
-
-
-
-
-
-
-
-
-```
-
-### `pyscript.document`
-
-On both main and worker threads, this object is a proxy for the web page's
-[document object](https://developer.mozilla.org/en-US/docs/Web/API/Document).
-The `document` is a representation of the
-[DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Using_the_Document_Object_Model)
-and can be used to read or manipulate the content of the web page.
-
-### `pyscript.fetch`
-
-A common task is to `fetch` data from the web via HTTP requests. The
-`pyscript.fetch` function provides a uniform way to achieve this in both
-Pyodide and MicroPython. It is closely modelled on the
-[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) found
-in browsers with some important Pythonic differences.
-
-The simple use case is to pass in a URL and `await` the response. If this
-request is in a function, that function should also be defined as `async`.
-
-```python title="A simple HTTP GET with pyscript.fetch"
-from pyscript import fetch
-
-
-response = await fetch("https://example.com")
-if response.ok:
- data = await response.text()
-else:
- print(response.status)
-```
-
-The object returned from an `await fetch` call will have attributes that
-correspond to the
-[JavaScript response object](https://developer.mozilla.org/en-US/docs/Web/API/Response).
-This is useful for getting response codes, headers and other metadata before
-processing the response's data.
-
-Alternatively, rather than using a double `await` (one to get the response, the
-other to grab the data), it's possible to chain the calls into a single
-`await` like this:
-
-```python title="A simple HTTP GET as a single await"
-from pyscript import fetch
-
-data = await fetch("https://example.com").text()
-```
-
-The following awaitable methods are available to you to access the data
-returned from the server:
-
-* `arrayBuffer()` returns a Python
- [memoryview](https://docs.python.org/3/library/stdtypes.html#memoryview) of
- the response. This is equivalent to the
- [`arrayBuffer()` method](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer)
- in the browser based `fetch` API.
-* `blob()` returns a JavaScript
- [`blob`](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob)
- version of the response. This is equivalent to the
- [`blob()` method](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob)
- in the browser based `fetch` API.
-* `bytearray()` returns a Python
- [`bytearray`](https://docs.python.org/3/library/stdtypes.html#bytearray)
- version of the response.
-* `json()` returns a Python datastructure representing a JSON serialised
- payload in the response.
-* `text()` returns a Python string version of the response.
-
-The underlying browser `fetch` API has
-[many request options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options)
-that you should simply pass in as keyword arguments like this:
-
-```python title="Supplying request options."
-from pyscript import fetch
-
-
-result = await fetch("https://example.com", method="POST", body="HELLO").text()
-```
-
-!!! Danger
-
- You may encounter
- [CORS](https://developer.mozilla.org/en-US/docs/Glossary/CORS)
- errors (especially with reference to a missing
- [Access-Control-Allow-Origin header](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin).
-
- This is a security feature of modern browsers where the site to which you
- are making a request **will not process a request from a site hosted at
- another domain**.
-
- For example, if your PyScript app is hosted under `example.com` and you
- make a request to `bbc.co.uk` (who don't allow requests from other domains)
- then you'll encounter this sort of CORS related error.
-
- There is nothing PyScript can do about this problem (it's a feature, not a
- bug). However, you could use a pass-through proxy service to get around
- this limitation (i.e. the proxy service makes the call on your behalf).
-
-### `pyscript.ffi`
-
-The `pyscript.ffi` namespace contains foreign function interface (FFI) methods
-that work in both Pyodide and MicroPython.
-
-#### `pyscript.ffi.create_proxy`
-
-A utility function explicitly for when a callback function is added via an
-event listener. It ensures the function still exists beyond the assignment of
-the function to an event. Should you not `create_proxy` around the callback
-function, it will be immediately garbage collected after being bound to the
-event.
-
-!!! warning
-
- There is some technical complexity to this situation, and we have attempted
- to create a mechanism where `create_proxy` is never needed.
-
- *Pyodide* expects the created proxy to be explicitly destroyed when it's
- not needed / used anymore. However, the underlying `proxy.destroy()` method
- has not been implemented in *MicroPython* (yet).
-
- To simplify this situation and automatically destroy proxies based on
- JavaScript memory management (garbage collection) heuristics, we have
- introduced an **experimental flag**:
-
- ```toml
- experimental_create_proxy = "auto"
- ```
-
- This flag ensures the proxy creation and destruction process is managed for
- you. When using this flag you should never need to explicitly call
- `create_proxy`.
-
-The technical details of how this works are
-[described here](../user-guide/ffi#create_proxy).
-
-#### `pyscript.ffi.to_js`
-
-A utility function to convert Python references into their JavaScript
-equivalents. For example, a Python dictionary is converted into a JavaScript
-object literal (rather than a JavaScript `Map`), unless a `dict_converter`
-is explicitly specified and the runtime is Pyodide.
-
-The technical details of how this works are [described here](../user-guide/ffi#to_js).
-
-### `pyscript.fs`
-
-!!! danger
-
- This API only works in Chromium based browsers.
-
-An API for mounting the user's local filesystem to a designated directory in
-the browser's virtual filesystem. Please see
-[the filesystem](../user-guide/filesystem) section of the user-guide for more
-information.
-
-#### `pyscript.fs.mount`
-
-Mount a directory on the user's local filesystem into the browser's virtual
-filesystem. If no previous
-[transient user activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation)
-has taken place, this function will result in a minimalist dialog to provide
-the required transient user activation.
-
-This asynchronous function takes four arguments:
-
-* `path` (required) - indicating the location on the in-browser filesystem to
- which the user selected directory from the local filesystem will be mounted.
-* `mode` (default: `"readwrite"`) - indicates how the code may interact with
- the mounted filesystem. May also be just `"read"` for read-only access.
-* `id` (default: `"pyscript"`) - indicate a unique name for the handler
- associated with a directory on the user's local filesystem. This allows users
- to select different folders and mount them at the same path in the
- virtual filesystem.
-* `root` (default: `""`) - a hint to the browser for where to start picking the
- path that should be mounted in Python. Valid values are: `desktop`,
- `documents`, `downloads`, `music`, `pictures` or `videos` as per
- [web standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#startin).
-
-```python title="Mount a local directory to the '/local' directory in the browser's virtual filesystem"
-from pyscript import fs
-
-
-# May ask for permission from the user, and select the local target.
-await fs.mount("/local")
-```
-
-If the call to `fs.mount` happens after a click or other transient event, the
-confirmation dialog will not be shown.
-
-```python title="Mounting without a transient event dialog."
-from pyscript import fs
-
-
-async def handler(event):
- """
- The click event that calls this handler is already a transient event.
- """
- await fs.mount("/local")
-
-
-my_button.onclick = handler
-```
-
-#### `pyscript.fs.sync`
-
-Given a named `path` for a mount point on the browser's virtual filesystem,
-asynchronously ensure the virtual and local directories are synchronised (i.e.
-all changes made in the browser's mounted filesystem, are propagated to the
-user's local filesystem).
-
-```python title="Synchronise the virtual and local filesystems."
-await fs.sync("/local")
-```
-
-#### `pyscript.fs.unmount`
-
-Asynchronously unmount the named `path` from the browser's virtual filesystem
-after ensuring content is synchronized. This will free up memory and allow you
-to re-use the path to mount a different directory.
-
-```python title="Unmount from the virtual filesystem."
-await fs.unmount("/local")
-```
-
-### `pyscript.js_modules`
-
-It is possible to [define JavaScript modules to use within your Python code](../user-guide/configuration#javascript-modules).
-
-Such named modules will always then be available under the
-`pyscript.js_modules` namespace.
-
-!!! warning
-
- Please see the documentation (linked above) about restrictions and gotchas
- when configuring how JavaScript modules are made available to PyScript.
-
-### `pyscript.media`
-
-The `pyscript.media` namespace provides classes and functions for interacting
-with media devices and streams in a web browser. This module enables you to work
-with cameras, microphones, and other media input/output devices directly from
-Python code.
-
-#### `pyscript.media.Device`
-
-A class that represents a media input or output device, such as a microphone,
-camera, or headset.
-
-```python title="Creating a Device object"
-from pyscript.media import Device, list_devices
-
-# List all available media devices
-devices = await list_devices()
-# Get the first available device
-my_device = devices[0]
-```
-
-The `Device` class has the following properties:
-
-* `id` - a unique string identifier for the represented device.
-* `group` - a string group identifier for devices belonging to the same physical device.
-* `kind` - an enumerated value: "videoinput", "audioinput", or "audiooutput".
-* `label` - a string describing the device (e.g., "External USB Webcam").
-
-The `Device` class also provides the following methods:
-
-##### `Device.load(audio=False, video=True)`
-
-A class method that loads a media stream with the specified options.
-
-```python title="Loading a media stream"
-# Load a video stream (default)
-stream = await Device.load()
-
-# Load an audio stream only
-stream = await Device.load(audio=True, video=False)
-
-# Load with specific video constraints
-stream = await Device.load(video={"width": 1280, "height": 720})
-```
-
-Parameters:
-* `audio` (bool, default: False) - Whether to include audio in the stream.
-* `video` (bool or dict, default: True) - Whether to include video in the
- stream. Can also be a dictionary of video constraints.
-
-Returns:
-* A media stream object that can be used with HTML media elements.
-
-##### `get_stream()`
-
-An instance method that gets a media stream from this specific device.
-
-```python title="Getting a stream from a specific device"
-# Find a video input device
-video_devices = [d for d in devices if d.kind == "videoinput"]
-if video_devices:
- # Get a stream from the first video device
- stream = await video_devices[0].get_stream()
-```
-
-Returns:
-* A media stream object from the specific device.
-
-#### `pyscript.media.list_devices()`
-
-An async function that returns a list of all currently available media input and
-output devices.
-
-```python title="Listing all media devices"
-from pyscript.media import list_devices
-
-devices = await list_devices()
-for device in devices:
- print(f"Device: {device.label}, Kind: {device.kind}")
-```
-
-Returns:
-* A list of `Device` objects representing the available media devices.
-
-!!! Note
-
- The returned list will omit any devices that are blocked by the document
- Permission Policy or for which the user has not granted permission.
-
-### Simple Example
-
-```python title="Basic camera access"
-from pyscript import document
-from pyscript.media import Device
-
-async def init_camera():
- # Get a video stream
- stream = await Device.load(video=True)
-
- # Set the stream as the source for a video element
- video_el = document.getElementById("camera")
- video_el.srcObject = stream
-
-# Initialize the camera
-init_camera()
-```
-
-!!! warning
-
- Using media devices requires appropriate permissions from the user.
- Browsers will typically show a permission dialog when `list_devices()` or
- `Device.load()` is called.
-
-### `pyscript.storage`
-
-The `pyscript.storage` API wraps the browser's built-in
-[IndexDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
-persistent storage in a synchronous Pythonic API.
-
-!!! info
-
- The storage API is persistent per user tab, page, or domain, in the same
- way IndexedDB persists.
-
- This API **is not** saving files in the interpreter's virtual file system
- nor onto the user's hard drive.
-
-```python
-from pyscript import storage
-
-
-# Each store must have a meaningful name.
-store = await storage("my-storage-name")
-
-# store is a dictionary and can now be used as such.
-```
-
-The returned dictionary automatically loads the current state of the referenced
-IndexDB. All changes are automatically queued in the background.
-
-```python
-# This is a write operation.
-store["key"] = value
-
-# This is also a write operation (it changes the stored data).
-del store["key"]
-```
-
-Should you wish to be certain changes have been synchronized to the underlying
-IndexDB, just `await store.sync()`.
-
-Common types of value can be stored via this API: `bool`, `float`, `int`, `str`
-and `None`. In addition, data structures like `list`, `dict` and `tuple` can
-be stored.
-
-!!! warning
-
- Because of the way the underlying data structure are stored in IndexDB,
- a Python `tuple` will always be returned as a Python `list`.
-
-It is even possible to store arbitrary data via a `bytearray` or
-`memoryview` object. However, there is a limitation that **such values must be
-stored as a single key/value pair, and not as part of a nested data
-structure**.
-
-Sometimes you may need to modify the behaviour of the `dict` like object
-returned by `pyscript.storage`. To do this, create a new class that inherits
-from `pyscript.Storage`, then pass in your class to `pyscript.storage` as the
-`storage_class` argument:
-
-```python
-from pyscript import window, storage, Storage
-
-
-class MyStorage(Storage):
-
- def __setitem__(self, key, value):
- super().__setitem__(key, value)
- window.console.log(key, value)
- ...
-
-
-store = await storage("my-data-store", storage_class=MyStorage)
-
-# The store object is now an instance of MyStorage.
-```
-
-### `@pyscript/core/donkey`
-
-Sometimes you need an asynchronous Python worker ready and waiting to evaluate
-any code on your behalf. This is the concept behind the JavaScript "donkey". We
-couldn't think of a better way than "donkey" to describe something that is easy
-to understand and shoulders the burden without complaint. This feature
-means you're able to use PyScript without resorting to specialised
-`
-
-
-```
-
-### `pyscript.RUNNING_IN_WORKER`
-
-This constant flag is `True` when the current code is running within a
-*worker*. It is `False` when the code is running within the *main* thread.
-
-### `pyscript.WebSocket`
-
-If a `pyscript.fetch` results in a call and response HTTP interaction with a
-web server, the `pyscript.Websocket` class provides a way to use
-[websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
-for two-way sending and receiving of data via a long term connection with a
-web server.
-
-PyScript's implementation, available in both the main thread and a web worker,
-closely follows the browser's own
-[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) class.
-
-This class accepts the following named arguments:
-
-* A `url` pointing at the _ws_ or _wss_ address. E.g.:
- `WebSocket(url="ws://localhost:5037/")`
-* Some `protocols`, an optional string or a list of strings as
- [described here](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#parameters).
-
-The `WebSocket` class also provides these convenient static constants:
-
-* `WebSocket.CONNECTING` (`0`) - the `ws.readyState` value when a web socket
- has just been created.
-* `WebSocket.OPEN` (`1`) - the `ws.readyState` value once the socket is open.
-* `WebSocket.CLOSING` (`2`) - the `ws.readyState` after `ws.close()` is
- explicitly invoked to stop the connection.
-* `WebSocket.CLOSED` (`3`) - the `ws.readyState` once closed.
-
-A `WebSocket` instance has only 2 methods:
-
-* `ws.send(data)` - where `data` is either a string or a Python buffer,
- automatically converted into a JavaScript typed array. This sends data via
- the socket to the connected web server.
-* `ws.close(code=0, reason="because")` - which optionally accepts `code` and
- `reason` as named arguments to signal some specific status or cause for
- closing the web socket. Otherwise `ws.close()` works with the default
- standard values.
-
-A `WebSocket` instance also has the fields that the JavaScript
-`WebSocket` instance will have:
-
-* [binaryType](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType) -
- the type of binary data being received over the WebSocket connection.
-* [bufferedAmount](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount) -
- a read-only property that returns the number of bytes of data that have been
- queued using calls to `send()` but not yet transmitted to the network.
-* [extensions](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions) -
- a read-only property that returns the extensions selected by the server.
-* [protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol) -
- a read-only property that returns the name of the sub-protocol the server
- selected.
-* [readyState](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) -
- a read-only property that returns the current state of the WebSocket
- connection as one of the `WebSocket` static constants (`CONNECTING`, `OPEN`,
- etc...).
-* [url](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/url) -
- a read-only property that returns the absolute URL of the `WebSocket`
- instance.
-
-A `WebSocket` instance can have the following listeners. Directly attach
-handler functions to them. Such functions will always receive a single
-`event` object.
-
-* [onclose](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event) -
- fired when the `WebSocket`'s connection is closed.
-* [onerror](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event) -
- fired when the connection is closed due to an error.
-* [onmessage](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) -
- fired when data is received via the `WebSocket`. If the `event.data` is a
- JavaScript typed array instead of a string, the reference it will point
- directly to a _memoryview_ of the underlying `bytearray` data.
-* [onopen](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event) -
- fired when the connection is opened.
-
-The following code demonstrates a `pyscript.WebSocket` in action.
-
-```html
-
-```
-
-!!! info
-
- It's also possible to pass in any handler functions as named arguments when
- you instantiate the `pyscript.WebSocket` class:
-
- ```python
- from pyscript import WebSocket
-
-
- def onmessage(event):
- print(event.type, event.data)
- ws.close()
-
-
- ws = WebSocket(url="ws://example.com/socket", onmessage=onmessage)
- ```
-
-### `pyscript.js_import`
-
-If a JavaScript module is only needed under certain circumstances, we provide
-an asynchronous way to import packages that were not originally referenced in
-your configuration.
-
-```html title="A pyscript.js_import example."
-
-```
-
-The `py_import` call returns an asynchronous tuple containing the Python
-modules provided by the packages referenced as string arguments.
-
-## Main-thread only features
-
-### `pyscript.PyWorker`
-
-A class used to instantiate a new worker from within Python.
-
-!!! Note
-
- Sometimes we disambiguate between interpreters through naming conventions
- (e.g. `py` or `mpy`).
-
- However, this class is always `PyWorker` and **the desired interpreter
- MUST be specified via a `type` option**. Valid values for the type of
- interpreter are either `micropython` or `pyodide`.
-
-The following fragments demonstrate how to evaluate the file `worker.py` on a
-new worker from within Python.
-
-```python title="worker.py - the file to run in the worker."
-from pyscript import RUNNING_IN_WORKER, display, sync
-
-display("Hello World", target="output", append=True)
-
-# will log into devtools console
-print(RUNNING_IN_WORKER) # True
-print("sleeping")
-sync.sleep(1)
-print("awake")
-```
-
-```python title="main.py - starts a new worker in Python."
-from pyscript import PyWorker
-
-# type MUST be either `micropython` or `pyodide`
-PyWorker("worker.py", type="micropython")
-```
-
-```html title="The HTML context for the worker."
-
-```
-
-While over on the main thread, this fragment of MicroPython will be able to
-access the worker's `version` function via the `workers` reference:
-
-```html
-
-```
-
-Importantly, the `workers` reference will **NOT** provide a list of
-known workers, but will only `await` for a reference to a named worker
-(resolving when the worker is ready). This is because the timing of worker
-startup is not deterministic.
-
-Should you wish to await for all workers on the page at load time, it's
-possible to loop over matching elements in the document like this:
-
-```html
-
-```
-
-## Worker only features
-
-### `pyscript.sync`
-
-A function used to pass serializable data from workers to the main thread.
-
-Imagine you have this code on the main thread:
-
-```python title="Python code on the main thread"
-from pyscript import PyWorker
-
-def hello(name="world"):
- display(f"Hello, {name}")
-
-worker = PyWorker("./worker.py")
-worker.sync.hello = hello
-```
-
-In the code on the worker, you can pass data back to handler functions like
-this:
-
-```python title="Pass data back to the main thread from a worker"
-from pyscript import sync
-
-sync.hello("PyScript")
-```
-
-## HTML attributes
-
-As a convenience, and to ensure backwards compatibility, PyScript allows the
-use of inline event handlers via custom HTML attributes.
-
-!!! warning
-
- This classic pattern of coding (inline event handlers) is no longer
- considered good practice in web development circles.
-
- We include this behaviour for historic reasons, but the folks at
- Mozilla [have a good explanation](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#inline_event_handlers_%E2%80%94_dont_use_these)
- of why this is currently considered bad practice.
-
-These attributes, expressed as `py-*` or `mpy-*` attributes of an HTML element,
-reference the name of a Python function to run when the event is fired. You
-should replace the `*` with the _actual name of an event_ (e.g. `py-click` or
-`mpy-click`). This is similar to how all
-[event handlers on elements](https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects)
-start with `on` in standard HTML (e.g. `onclick`). The rule of thumb is to
-simply replace `on` with `py-` or `mpy-` and then reference the name of a
-Python function.
-
-```html title="A py-click event on an HTML button element."
-Click me!
-```
-
-```python title="The related Python function."
-from pyscript import window
-
-
-def handle_click(event):
- """
- Simply log the click event to the browser's console.
- """
- window.console.log(event)
-```
-
-Under the hood, the [`pyscript.when`](#pyscriptwhen) decorator is used to
-enable this behaviour.
-
-!!! note
-
- In earlier versions of PyScript, the value associated with the attribute
- was simply evaluated by the Python interpreter. This was unsafe:
- manipulation of the attribute's value could have resulted in the evaluation
- of arbitrary code.
-
- This is why we changed to the current behaviour: just supply the name
- of the Python function to be evaluated, and PyScript will do this safely.
diff --git a/docs/api/context.md b/docs/api/context.md
new file mode 100644
index 0000000..635214e
--- /dev/null
+++ b/docs/api/context.md
@@ -0,0 +1,3 @@
+# `pyscript.context`
+
+::: pyscript.context
diff --git a/docs/api/display.md b/docs/api/display.md
new file mode 100644
index 0000000..fb98686
--- /dev/null
+++ b/docs/api/display.md
@@ -0,0 +1,3 @@
+# `pyscript.display`
+
+::: pyscript.display
diff --git a/docs/api/events.md b/docs/api/events.md
new file mode 100644
index 0000000..061c4ae
--- /dev/null
+++ b/docs/api/events.md
@@ -0,0 +1,3 @@
+# `pyscript.event`
+
+::: pyscript.events
diff --git a/docs/api/fetch.md b/docs/api/fetch.md
new file mode 100644
index 0000000..c57937f
--- /dev/null
+++ b/docs/api/fetch.md
@@ -0,0 +1,3 @@
+# `pyscript.fetch`
+
+::: pyscript.fetch
diff --git a/docs/api/ffi.md b/docs/api/ffi.md
new file mode 100644
index 0000000..69b5472
--- /dev/null
+++ b/docs/api/ffi.md
@@ -0,0 +1,3 @@
+# `pyscript.ffi`
+
+::: pyscript.ffi
diff --git a/docs/api/flatted.md b/docs/api/flatted.md
new file mode 100644
index 0000000..e8052ff
--- /dev/null
+++ b/docs/api/flatted.md
@@ -0,0 +1,3 @@
+# `pyscript.flatted`
+
+::: pyscript.flatted
diff --git a/docs/api/fs.md b/docs/api/fs.md
new file mode 100644
index 0000000..6ef3363
--- /dev/null
+++ b/docs/api/fs.md
@@ -0,0 +1,3 @@
+# `pyscript.fs`
+
+::: pyscript.fs
diff --git a/docs/api/init.md b/docs/api/init.md
new file mode 100644
index 0000000..2da87b0
--- /dev/null
+++ b/docs/api/init.md
@@ -0,0 +1,14 @@
+# The `pyscript` API
+
+!!! important
+
+ These API docs are auto-generated from our source code. To suggest
+ changes or report errors, please do so via
+ [our GitHub repository](https://github.com/pyscript/pyscript). The
+ source code for these APIs
+ [is found here](https://github.com/pyscript/pyscript/tree/main/core/src/stdlib/pyscript)
+ in our repository.
+
+::: pyscript
+ options:
+ show_root_heading: false
diff --git a/docs/api/media.md b/docs/api/media.md
new file mode 100644
index 0000000..1b19777
--- /dev/null
+++ b/docs/api/media.md
@@ -0,0 +1,3 @@
+# `pyscript.media`
+
+::: pyscript.media
diff --git a/docs/api/storage.md b/docs/api/storage.md
new file mode 100644
index 0000000..053df3c
--- /dev/null
+++ b/docs/api/storage.md
@@ -0,0 +1,3 @@
+# `pyscript.storage`
+
+::: pyscript.storage
diff --git a/docs/api/util.md b/docs/api/util.md
new file mode 100644
index 0000000..d7e7db4
--- /dev/null
+++ b/docs/api/util.md
@@ -0,0 +1,3 @@
+# `pyscript.util`
+
+::: pyscript.util
diff --git a/docs/api/web.md b/docs/api/web.md
new file mode 100644
index 0000000..b1a3c1c
--- /dev/null
+++ b/docs/api/web.md
@@ -0,0 +1,18 @@
+# `pyscript.web`
+
+::: pyscript.web
+ options:
+ members:
+ - page
+ - Element
+ - ContainerElement
+ - ElementCollection
+ - Classes
+ - Style
+ - HasOptions
+ - Options
+ - Page
+ - canvas
+ - video
+ - CONTAINER_TAGS
+ - VOID_TAGS
diff --git a/docs/api/websocket.md b/docs/api/websocket.md
new file mode 100644
index 0000000..093fda8
--- /dev/null
+++ b/docs/api/websocket.md
@@ -0,0 +1,3 @@
+# `pyscript.websocket`
+
+::: pyscript.websocket
diff --git a/docs/api/workers.md b/docs/api/workers.md
new file mode 100644
index 0000000..2930a8d
--- /dev/null
+++ b/docs/api/workers.md
@@ -0,0 +1,3 @@
+# `pyscript.workers`
+
+::: pyscript.workers
diff --git a/mkdocs.yml b/mkdocs.yml
index d62e421..280b6a3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,4 +1,6 @@
site_name: PyScript
+site_author: The PyScript OSS Team
+site_description: PyScript - an open source platform for Python in the browser.
theme:
name: material
@@ -58,6 +60,16 @@ plugins:
css_dir: css
javascript_dir: js
canonical_version: null
+ - mkdocstrings:
+ default_handler: python
+ locale: en
+ handlers:
+ python:
+ options:
+ show_source: true
+ members_order: source
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
nav:
- Home: index.md
@@ -80,7 +92,21 @@ nav:
- PyGame-CE: user-guide/pygame-ce.md
- Plugins: user-guide/plugins.md
- Use Offline: user-guide/offline.md
- - Built-in APIs: api.md
+ - PyScript APIs:
+ - Introduction: api/init.md
+ - context: api/context.md
+ - display: api/display.md
+ - events: api/events.md
+ - fetch: api/fetch.md
+ - ffi: api/ffi.md
+ - flatted: api/flatted.md
+ - fs: api/fs.md
+ - media: api/media.md
+ - storage: api/storage.md
+ - util: api/util.md
+ - web: api/web.md
+ - websocket: api/websocket.md
+ - workers: api/workers.md
- FAQ: faq.md
- Contributing: contributing.md
- Developer Guide: developers.md
diff --git a/pyscript/__init__.py b/pyscript/__init__.py
new file mode 100644
index 0000000..644ec56
--- /dev/null
+++ b/pyscript/__init__.py
@@ -0,0 +1,120 @@
+"""
+This is the main `pyscript` namespace. It provides the primary Pythonic API
+for users to interact with the
+[browser's own API](https://developer.mozilla.org/en-US/docs/Web/API). It
+includes utilities for common activities such as displaying content, handling
+events, fetching resources, managing local storage, and coordinating with
+web workers.
+
+The most important names provided by this namespace can be directly imported
+from `pyscript`, for example:
+
+```python
+from pyscript import display, HTML, fetch, when, storage, WebSocket
+```
+
+The following names are available in the `pyscript` namespace:
+
+- `RUNNING_IN_WORKER`: Boolean indicating if the code is running in a Web
+ Worker.
+- `PyWorker`: Class for creating Web Workers running Python code.
+- `config`: Configuration object for pyscript settings.
+- `current_target`: The element in the DOM that is the current target for
+ output.
+- `document`: The standard `document` object, proxied in workers.
+- `window`: The standard `window` object, proxied in workers.
+- `js_import`: Function to dynamically import JS modules.
+- `js_modules`: Object containing JS modules available to Python.
+- `sync`: Utility for synchronizing between worker and main thread.
+- `display`: Function to render Python objects in the web page.
+- `HTML`: Helper class to create HTML content for display.
+- `fetch`: Function to perform HTTP requests.
+- `Storage`: Class representing browser storage (local/session).
+- `storage`: Object to interact with browser's local storage.
+- `WebSocket`: Class to create and manage WebSocket connections.
+- `when`: Function to register event handlers on DOM elements.
+- `Event`: Class representing user defined or DOM events.
+- `py_import`: Function to lazily import Pyodide related Python modules.
+
+If running in the main thread, the following additional names are available:
+
+- `create_named_worker`: Function to create a named Web Worker.
+- `workers`: Object to manage and interact with existing Web Workers.
+
+All of these names are defined in the various submodules of `pyscript` and
+are imported and re-exported here for convenience. Please refer to the
+respective submodule documentation for more details on each component.
+
+
+!!! Note
+ Some notes about the naming conventions and the relationship between
+ various similar-but-different names found within this code base.
+
+ ```python
+ import pyscript
+ ```
+
+ The `pyscript` package contains the main user-facing API offered by
+ PyScript. All the names which are supposed be used by end users should
+ be made available in `pyscript/__init__.py` (i.e., this source file).
+
+ ```python
+ import _pyscript
+ ```
+
+ The `_pyscript` module is an internal API implemented in JS. **End users
+ should not use it directly**. For its implementation, grep for
+ `interpreter.registerJsModule("_pyscript",...)` in `core.js`.
+
+ ```python
+ import js
+ ```
+
+ The `js` object is
+ [the JS `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis),
+ as exported by Pyodide and/or Micropython's foreign function interface
+ (FFI). As such, it contains different things in the main thread or in a
+ worker, as defined by web standards.
+
+ ```python
+ import pyscript.context
+ ```
+
+ The `context` submodule abstracts away some of the differences between
+ the main thread and a worker. Its most important features are made
+ available in the root `pyscript` namespace. All other functionality is
+ mostly for internal PyScript use or advanced users. In particular, it
+ defines `window` and `document` in such a way that these names work in
+ both cases: in the main thread, they are the "real" objects, in a worker
+ they are proxies which work thanks to
+ [coincident](https://github.com/WebReflection/coincident).
+
+ ```python
+ from pyscript import window, document
+ ```
+
+ These are just the `window` and `document` objects as defined by
+ `pyscript.context`. This is the blessed way to access them from `pyscript`,
+ as it works transparently in both the main thread and worker cases.
+"""
+
+from polyscript import lazy_py_modules as py_import
+from pyscript.context import (
+ RUNNING_IN_WORKER,
+ PyWorker,
+ config,
+ current_target,
+ document,
+ js_import,
+ js_modules,
+ sync,
+ window,
+)
+from pyscript.display import HTML, display
+from pyscript.fetch import fetch
+from pyscript.storage import Storage, storage
+from pyscript.websocket import WebSocket
+from pyscript.events import when, Event
+
+if not RUNNING_IN_WORKER:
+ from pyscript.workers import create_named_worker, workers
diff --git a/pyscript/context.py b/pyscript/context.py
new file mode 100644
index 0000000..b3f775c
--- /dev/null
+++ b/pyscript/context.py
@@ -0,0 +1,198 @@
+"""
+Execution context management for PyScript.
+
+This module handles the differences between running in the
+[main browser thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread)
+versus running in a
+[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers),
+providing a consistent API regardless of the execution context.
+
+Key features:
+
+- Detects whether code is running in a worker or main thread. Read this via
+ the boolean `pyscript.context.RUNNING_IN_WORKER`.
+- Parses and normalizes configuration from `polyscript.config` and adds the
+ Python interpreter type via the `type` key in `pyscript.context.config`.
+- Provides appropriate implementations of `window`, `document`, and `sync`.
+- Sets up JavaScript module import system, including a lazy `js_import`
+ function.
+- Manages `PyWorker` creation.
+- Provides access to the current display target via
+ `pyscript.context.display_target`.
+
+!!! warning
+
+ These are key differences between the main thread and worker contexts:
+
+ Main thread context:
+
+ - `window` and `document` are available directly.
+ - `PyWorker` can be created to spawn worker threads.
+ - `sync` is not available (raises `NotSupported`).
+
+ Worker context:
+
+ - `window` and `document` are proxied from main thread (if SharedArrayBuffer
+ available).
+ - `PyWorker` is not available (raises `NotSupported`).
+ - `sync` utilities are available for main thread communication.
+"""
+
+import json
+import sys
+
+import js
+from polyscript import config as _polyscript_config
+from polyscript import js_modules
+from pyscript.util import NotSupported
+
+RUNNING_IN_WORKER = not hasattr(js, "document")
+"""Detect execution context: True if running in a worker, False if main thread."""
+
+config = json.loads(js.JSON.stringify(_polyscript_config))
+"""Parsed and normalized configuration."""
+if isinstance(config, str):
+ config = {}
+
+js_import = None
+"""Function to import JavaScript modules dynamically."""
+
+window = None
+"""The `window` object (proxied if in a worker)."""
+
+document = None
+"""The `document` object (proxied if in a worker)."""
+
+sync = None
+"""Sync utilities for worker-main thread communication (only in workers)."""
+
+# Detect and add Python interpreter type to config.
+if "MicroPython" in sys.version:
+ config["type"] = "mpy"
+else:
+ config["type"] = "py"
+
+
+class _JSModuleProxy:
+ """
+ Proxy for JavaScript modules imported via js_modules.
+
+ This allows Python code to import JavaScript modules using Python's
+ import syntax:
+
+ ```python
+ from pyscript.js_modules lodash import debounce
+ ```
+
+ The proxy lazily retrieves the actual JavaScript module when accessed.
+ """
+
+ def __init__(self, name):
+ """
+ Create a proxy for the named JavaScript module.
+ """
+ self.name = name
+
+ def __getattr__(self, field):
+ """
+ Retrieve a JavaScript object/function from the proxied JavaScript
+ module via the given `field` name.
+ """
+ # Avoid Pyodide looking for non-existent special methods.
+ if not field.startswith("_"):
+ return getattr(getattr(js_modules, self.name), field)
+ return None
+
+
+# Register all available JavaScript modules in Python's module system.
+# This enables: from pyscript.js_modules.xxx import yyy
+for module_name in js.Reflect.ownKeys(js_modules):
+ sys.modules[f"pyscript.js_modules.{module_name}"] = _JSModuleProxy(module_name)
+sys.modules["pyscript.js_modules"] = js_modules
+
+
+# Context-specific setup: Worker vs Main Thread.
+if RUNNING_IN_WORKER:
+ import polyscript
+
+ # PyWorker cannot be created from within a worker.
+ PyWorker = NotSupported(
+ "pyscript.PyWorker",
+ "pyscript.PyWorker works only when running in the main thread",
+ )
+
+ # Attempt to access main thread's window and document via SharedArrayBuffer.
+ try:
+ window = polyscript.xworker.window
+ document = window.document
+ js.document = document
+
+ # Create js_import function that runs imports on the main thread.
+ js_import = window.Function(
+ "return (...urls) => Promise.all(urls.map((url) => import(url)))"
+ )()
+
+ except:
+ # SharedArrayBuffer not available - window/document cannot be proxied.
+ sab_error_message = (
+ "Unable to use `window` or `document` in worker. "
+ "This requires SharedArrayBuffer support. "
+ "See: https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
+ )
+ js.console.warn(sab_error_message)
+ window = NotSupported("pyscript.window", sab_error_message)
+ document = NotSupported("pyscript.document", sab_error_message)
+
+ # Worker-specific utilities for main thread communication.
+ sync = polyscript.xworker.sync
+
+ def current_target():
+ """
+ Get the current output target in worker context.
+ """
+ return polyscript.target
+
+else:
+ # Main thread context setup.
+ import _pyscript
+ from _pyscript import PyWorker as _PyWorker
+ from pyscript.ffi import to_js
+
+ js_import = _pyscript.js_import
+
+ def PyWorker(url, **options):
+ """
+ Create a Web Worker running Python code.
+
+ This spawns a new worker thread that can execute Python code
+ found at the `url`, independently of the main thread. The
+ `**options` can be used to configure the worker.
+
+ ```python
+ from pyscript import PyWorker
+
+
+ # Create a worker to run background tasks.
+ # (`type` MUST be either `micropython` or `pyodide`)
+ worker = PyWorker("./worker.py", type="micropython")
+ ```
+
+ PyWorker **can only be created from the main thread**, not from
+ within another worker.
+ """
+ return _PyWorker(url, to_js(options))
+
+ # Main thread has direct access to window and document.
+ window = js
+ document = js.document
+
+ # sync is not available in main thread (only in workers).
+ sync = NotSupported(
+ "pyscript.sync", "pyscript.sync works only when running in a worker"
+ )
+
+ def current_target():
+ """
+ Get the current output target in main thread context.
+ """
+ return _pyscript.target
diff --git a/pyscript/display.py b/pyscript/display.py
new file mode 100644
index 0000000..69efd5d
--- /dev/null
+++ b/pyscript/display.py
@@ -0,0 +1,261 @@
+"""
+Display Pythonic content in the browser.
+
+This module provides the `display()` function for rendering Python objects
+in the web page. The function introspects objects to determine the appropriate
+[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types)
+and rendering method.
+
+Supported MIME types:
+
+- `text/plain`: Plain text (HTML-escaped)
+- `text/html`: HTML content
+- `image/png`: PNG images as data URLs
+- `image/jpeg`: JPEG images as data URLs
+- `image/svg+xml`: SVG graphics
+- `application/json`: JSON data
+- `application/javascript`: JavaScript code (discouraged)
+
+The `display()` function uses standard Python representation methods
+(`_repr_html_`, `_repr_png_`, etc.) to determine how to render objects.
+Objects can provide a `_repr_mimebundle_` method to specify preferred formats
+like this:
+
+```python
+def _repr_mimebundle_(self):
+ return {
+ "text/html": "Bold HTML ",
+ "image/png": "",
+ }
+```
+
+Heavily inspired by
+[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html).
+"""
+
+import base64
+import html
+import io
+from collections import OrderedDict
+from pyscript.context import current_target, document, window
+from pyscript.ffi import is_none
+
+
+def _render_image(mime, value, meta):
+ """
+ Render image (`mime`) data (`value`) as an HTML img element with data URL.
+ Any `meta` attributes are added to the img tag.
+
+ Accepts both raw bytes and base64-encoded strings for flexibility.
+ """
+ if isinstance(value, bytes):
+ value = base64.b64encode(value).decode("utf-8")
+ attrs = "".join([f' {k}="{v}"' for k, v in meta.items()])
+ return f' '
+
+
+# Maps MIME types to rendering functions.
+_MIME_TO_RENDERERS = {
+ "text/plain": lambda v, m: html.escape(v),
+ "text/html": lambda v, m: v,
+ "image/png": lambda v, m: _render_image("image/png", v, m),
+ "image/jpeg": lambda v, m: _render_image("image/jpeg", v, m),
+ "image/svg+xml": lambda v, m: v,
+ "application/json": lambda v, m: v,
+ "application/javascript": lambda v, m: f"
+
+
+
+```
+
+Dynamically creating named workers:
+
+```python
+from pyscript import create_named_worker
+
+
+# Create a worker from a Python file.
+worker = await create_named_worker(
+ src="./background_tasks.py",
+ name="task-processor"
+)
+
+# Use the worker's exported functions.
+result = await worker.process_data([1, 2, 3, 4, 5])
+print(result)
+```
+
+Key features:
+- Access (`await`) named workers via dictionary-like syntax.
+- Dynamically create workers from Python.
+- Cross-interpreter support (Pyodide and MicroPython).
+
+Worker access is asynchronous - you must `await workers[name]` to get
+a reference to the worker. This is because workers may not be ready
+immediately at startup.
+"""
+
+import js
+import json
+from polyscript import workers as _polyscript_workers
+
+
+class _ReadOnlyWorkersProxy:
+ """
+ A read-only proxy for accessing named web workers. Use
+ `create_named_worker()` to create new workers found in this proxy.
+
+ This provides dictionary-like access to named workers defined in
+ the page. It handles differences between Pyodide and MicroPython
+ implementations transparently.
+
+ (See: https://github.com/pyscript/pyscript/issues/2106 for context.)
+
+ The proxy is read-only to prevent accidental modification of the
+ underlying workers registry. Both item access and attribute access are
+ supported for convenience (especially since HTML attribute names may
+ not be valid Python identifiers).
+
+ ```python
+ from pyscript import workers
+
+ # Access a named worker.
+ my_worker = await workers["worker-name"]
+ result = await my_worker.some_function()
+
+ # Alternatively, if the name works, access via attribute notation.
+ my_worker = await workers.worker_name
+ result = await my_worker.some_function()
+ ```
+
+ **This is a proxy object, not a dict**. You cannot iterate over it or
+ get a list of worker names. This is intentional because worker
+ startup timing is non-deterministic.
+ """
+
+ def __getitem__(self, name):
+ """
+ Get a named worker by `name`. It returns a promise that resolves to
+ the worker reference when ready.
+
+ This is useful if the underlying worker name is not a valid Python
+ identifier.
+
+ ```python
+ worker = await workers["my-worker"]
+ ```
+ """
+ return js.Reflect.get(_polyscript_workers, name)
+
+ def __getattr__(self, name):
+ """
+ Get a named worker as an attribute. It returns a promise that resolves
+ to the worker reference when ready.
+
+ This allows accessing workers via dot notation as an alternative
+ to bracket notation.
+
+ ```python
+ worker = await workers.my_worker
+ ```
+ """
+ return js.Reflect.get(_polyscript_workers, name)
+
+
+# Global workers proxy for accessing named workers.
+workers = _ReadOnlyWorkersProxy()
+"""Global proxy for accessing named web workers."""
+
+
+async def create_named_worker(src, name, config=None, type="py"):
+ """
+ Dynamically create a web worker with a `src` Python file, a unique
+ `name` and optional `config` (dict or JSON string) and `type` (`py`
+ for Pyodide or `mpy` for MicroPython, the default is `py`).
+
+ This function creates a new web worker by injecting a `