From c5a3d1542087454584c853ee419cb3995c6ba0e5 Mon Sep 17 00:00:00 2001
From: "Nicholas H.Tollervey"
Date: Thu, 4 Dec 2025 15:24:19 +0000
Subject: [PATCH 01/10] Update requirements.
---
requirements.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 3eaa631..6d18dce 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
-mkdocs-material==9.3.1
+mkdocs-material==9.6.15
+mkdocstrings-python==1.16.12
mike==1.1.2
setuptools
From 6171c0aaf64e9819656987c10db9d8ce38b2a654 Mon Sep 17 00:00:00 2001
From: "Nicholas H.Tollervey"
Date: Thu, 4 Dec 2025 15:35:49 +0000
Subject: [PATCH 02/10] Add mkdocstrings and some extra meta-data to mkdocs.yml
for site wide configuration.
---
mkdocs.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/mkdocs.yml b/mkdocs.yml
index d62e421..19298e6 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,7 @@ plugins:
css_dir: css
javascript_dir: js
canonical_version: null
+ - mkdocstrings
nav:
- Home: index.md
From d0df6fb91b43408fdf0cc93fdcc6059e883a6de8 Mon Sep 17 00:00:00 2001
From: "Nicholas H.Tollervey"
Date: Thu, 4 Dec 2025 16:26:58 +0000
Subject: [PATCH 03/10] Autogenerate within markdown.
---
docs/api.md | 1286 ++-------------------------------------------------
1 file changed, 28 insertions(+), 1258 deletions(-)
diff --git a/docs/api.md b/docs/api.md
index 21327b0..8285846 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,1287 +1,57 @@
# Built-in APIs
-PyScript makes available convenience objects, functions and attributes.
+## PyScript
-In Python this is done via the builtin `pyscript` module:
+::: pyscript
-```python title="Accessing the document object via the pyscript module"
-from pyscript import document
-```
+## Context
-In HTML this is done via `py-*` and `mpy-*` attributes (depending on the
-interpreter you're using):
+::: pyscript.context
-```html title="An example of a py-click handler"
-Click me
-```
+## Display
-These APIs will work with both Pyodide and Micropython in exactly the same way.
+::: pyscript.display
-!!! info
+## Events
- Both Pyodide and MicroPython provide access to two further lower-level
- APIs:
+::: pyscript.events
- * 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.
+## Fetch
-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.
+::: pyscript.fetch
-## Common features
+## FFI
-These Python objects / functions are available in both the main thread and in
-code running on a web worker:
+::: pyscript.ffi
-### `pyscript.config`
+## Flatted
-A Python dictionary representing the configuration for the interpreter.
+::: pyscript.flatted
-```python title="Reading the current configuration."
-from pyscript import config
+## FS
+::: pyscript.fs
-# It's just a dict.
-print(config.get("files"))
-# This will be either "mpy" or "py" depending on the current interpreter.
-print(config["type"])
-```
+## Media
-!!! info
+::: pyscript.media
- 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.
+## Storage
-!!! warning
+::: pyscript.storage
- Changing the `config` dictionary at runtime has no effect on the actual
- configuration.
+## Util
- It's just a convenience to **read the configuration** at run time.
+::: pyscript.util
-### `pyscript.current_target`
+## Web
-A utility function to retrieve the unique identifier of the element used
-to display content. If the element is not a `
-```
+## Workers
-!!! 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.
+::: pyscript.workers
From d7de8357869007069b71961f62ab784774af8dcd Mon Sep 17 00:00:00 2001
From: "Nicholas H.Tollervey"
Date: Thu, 4 Dec 2025 16:27:36 +0000
Subject: [PATCH 04/10] Grab the pyscript namespace from the referenced
release, and put it somewhere the docs can find it.
---
version-update.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/version-update.js b/version-update.js
index c8f6d0b..a2e5165 100644
--- a/version-update.js
+++ b/version-update.js
@@ -24,3 +24,60 @@ const patch = directory => {
};
patch(join(__dirname, 'docs'));
+
+// Download and extract PyScript source code for the current version.
+const { execSync } = require('child_process');
+const { mkdtempSync, rmSync, cpSync } = require('fs');
+const { tmpdir } = require('os');
+
+const downloadFileSync = (url, destination) => {
+ // Use curl which is available on Mac and Linux.
+ try {
+ execSync(`curl -L -o "${destination}" "${url}"`, {
+ stdio: 'ignore'
+ });
+ } catch (error) {
+ throw new Error(`Download failed: ${error.message}`);
+ }
+};
+
+const updatePyScriptSource = () => {
+ const url = `https://github.com/pyscript/pyscript/archive/refs/tags/${version}.zip`;
+ const tempDir = mkdtempSync(join(tmpdir(), 'pyscript-'));
+ const zipPath = join(tempDir, `pyscript-${version}.zip`);
+ const targetDir = join(__dirname, 'pyscript');
+
+ try {
+ console.log(`Downloading PyScript ${version}...`);
+ downloadFileSync(url, zipPath);
+
+ console.log('Extracting archive...');
+ execSync(`unzip -q "${zipPath}" -d "${tempDir}"`);
+
+ const sourceDir = join(
+ tempDir,
+ `pyscript-${version}`,
+ 'core',
+ 'src',
+ 'stdlib',
+ 'pyscript'
+ );
+
+ if (!statSync(sourceDir, { throwIfNoEntry: false })?.isDirectory()) {
+ throw new Error(`Expected directory not found: ${sourceDir}`);
+ }
+
+ console.log('Copying PyScript stdlib files...');
+ cpSync(sourceDir, targetDir, { recursive: true, force: true });
+
+ console.log('PyScript source updated successfully.');
+ } catch (error) {
+ console.error('Error updating PyScript source:', error.message);
+ process.exit(1);
+ } finally {
+ console.log('Cleaning up temporary files...');
+ rmSync(tempDir, { recursive: true, force: true });
+ }
+};
+
+updatePyScriptSource();
From 896ca847b7ebad60cdf6dcab5f2f68c20c75c6cd Mon Sep 17 00:00:00 2001
From: "Nicholas H.Tollervey"
Date: Thu, 4 Dec 2025 16:29:15 +0000
Subject: [PATCH 05/10] Add latest version of the pyscript namespace from which
the API docs can be generated.
---
pyscript/__init__.py | 64 ++
pyscript/context.py | 175 +++++
pyscript/display.py | 260 ++++++++
pyscript/events.py | 223 +++++++
pyscript/fetch.py | 218 +++++++
pyscript/ffi.py | 161 +++++
pyscript/flatted.py | 223 +++++++
pyscript/fs.py | 257 ++++++++
pyscript/media.py | 244 +++++++
pyscript/storage.py | 246 +++++++
pyscript/util.py | 77 +++
pyscript/web.py | 1410 +++++++++++++++++++++++++++++++++++++++++
pyscript/websocket.py | 298 +++++++++
pyscript/workers.py | 191 ++++++
14 files changed, 4047 insertions(+)
create mode 100644 pyscript/__init__.py
create mode 100644 pyscript/context.py
create mode 100644 pyscript/display.py
create mode 100644 pyscript/events.py
create mode 100644 pyscript/fetch.py
create mode 100644 pyscript/ffi.py
create mode 100644 pyscript/flatted.py
create mode 100644 pyscript/fs.py
create mode 100644 pyscript/media.py
create mode 100644 pyscript/storage.py
create mode 100644 pyscript/util.py
create mode 100644 pyscript/web.py
create mode 100644 pyscript/websocket.py
create mode 100644 pyscript/workers.py
diff --git a/pyscript/__init__.py b/pyscript/__init__.py
new file mode 100644
index 0000000..58e24bd
--- /dev/null
+++ b/pyscript/__init__.py
@@ -0,0 +1,64 @@
+"""
+This is the main `pyscript` namespace. It provides the primary Pythonic API
+for users to interact with PyScript features sitting on top of 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.
+
+Some notes about the naming conventions and the relationship between various
+similar-but-different names found within this code base.
+
+`import pyscript`
+
+This 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 file).
+
+`import _pyscript`
+
+This is an internal module implemented in JS. It is used internally by
+the pyscript package, **end users should not use it directly**. For its
+implementation, grep for `interpreter.registerJsModule("_pyscript",
+...)` in `core.js`.
+
+`import js`
+
+This is the JS `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.
+
+`import pyscript.context`
+
+This submodule abstracts away some of the differences between the main
+thread and a worker. 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).
+
+`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..b5d8490
--- /dev/null
+++ b/pyscript/context.py
@@ -0,0 +1,175 @@
+"""
+Execution context management for PyScript.
+
+This module handles the differences between running in the main browser thread
+versus running in a Web Worker, 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
+ `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`.
+
+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
+
+# Detect execution context: True if running in a worker, False if main thread.
+RUNNING_IN_WORKER = not hasattr(js, "document")
+
+# Parse and normalize configuration from polyscript.
+config = json.loads(js.JSON.stringify(_polyscript_config))
+if isinstance(config, str):
+ config = {}
+
+# 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)
+ js_import = None
+
+ # 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, js_import
+ from pyscript.ffi import to_js
+
+ 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..0af530a
--- /dev/null
+++ b/pyscript/display.py
@@ -0,0 +1,260 @@
+"""
+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 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.
+Object 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. See:
+
+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()
+
+
+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 script tag into
+ the document. The worker will be accessible via the `workers` proxy once
+ it's ready.
+
+ It return a promise that resolves to the worker reference when ready.
+
+ ```python
+ from pyscript import create_named_worker
+
+
+ # Create a Pyodide worker.
+ worker = await create_named_worker(
+ src="./my_worker.py",
+ name="background-worker"
+ )
+
+ # Use the worker.
+ result = await worker.process_data()
+
+ # Create with standard PyScript configuration.
+ worker = await create_named_worker(
+ src="./processor.py",
+ name="data-processor",
+ config={"packages": ["numpy", "pandas"]}
+ )
+
+ # Use MicroPython instead.
+ worker = await create_named_worker(
+ src="./lightweight_worker.py",
+ name="micro-worker",
+ type="mpy"
+ )
+ ```
+
+ **The worker script should define** `__export__` to specify which
+ functions or objects are accessible from the main thread.
+ """
+ # Create script element for the worker.
+ script = js.document.createElement("script")
+ script.type = type
+ script.src = src
+ # Mark as a worker with a name.
+ script.setAttribute("worker", "")
+ script.setAttribute("name", name)
+ # Add configuration if provided.
+ if config:
+ if isinstance(config, str):
+ config_str = config
+ else:
+ config_str = json.dumps(config)
+ script.setAttribute("config", config_str)
+ # Inject the script into the document and await the result.
+ js.document.body.append(script)
+ return await workers[name]
From d055cf428eb4f0b5b6ccb8d03396e09e1839d9de Mon Sep 17 00:00:00 2001
From: "Nicholas H.Tollervey"
Date: Mon, 8 Dec 2025 17:12:55 +0000
Subject: [PATCH 06/10] Update the structure of the API docs into separate
pages, one for each submodule. All to be autogenerated from the source files
in ./pyscript.
---
docs/api.md | 57 ------------------
docs/api/context.md | 3 +
docs/api/display.md | 3 +
docs/api/events.md | 3 +
docs/api/fetch.md | 3 +
docs/api/ffi.md | 3 +
docs/api/flatted.md | 3 +
docs/api/fs.md | 3 +
docs/api/init.md | 14 +++++
docs/api/media.md | 3 +
docs/api/storage.md | 3 +
docs/api/util.md | 3 +
docs/api/web.md | 18 ++++++
docs/api/websocket.md | 3 +
docs/api/workers.md | 3 +
mkdocs.yml | 26 ++++++++-
pyscript/__init__.py | 113 ++++++++++++++++++++++++++----------
pyscript/context.py | 59 +++++++++++++------
pyscript/display.py | 27 ++++-----
pyscript/events.py | 5 +-
pyscript/fetch.py | 16 +++---
pyscript/ffi.py | 30 +++++-----
pyscript/flatted.py | 20 ++++---
pyscript/fs.py | 31 +++++-----
pyscript/media.py | 57 +++++++++---------
pyscript/storage.py | 40 +++++++------
pyscript/util.py | 16 +++---
pyscript/web.py | 131 ++++++++++++++++++++++++++----------------
pyscript/websocket.py | 65 ++++++++++-----------
pyscript/workers.py | 23 ++++----
30 files changed, 471 insertions(+), 313 deletions(-)
delete mode 100644 docs/api.md
create mode 100644 docs/api/context.md
create mode 100644 docs/api/display.md
create mode 100644 docs/api/events.md
create mode 100644 docs/api/fetch.md
create mode 100644 docs/api/ffi.md
create mode 100644 docs/api/flatted.md
create mode 100644 docs/api/fs.md
create mode 100644 docs/api/init.md
create mode 100644 docs/api/media.md
create mode 100644 docs/api/storage.md
create mode 100644 docs/api/util.md
create mode 100644 docs/api/web.md
create mode 100644 docs/api/websocket.md
create mode 100644 docs/api/workers.md
diff --git a/docs/api.md b/docs/api.md
deleted file mode 100644
index 8285846..0000000
--- a/docs/api.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# Built-in APIs
-
-## PyScript
-
-::: pyscript
-
-## Context
-
-::: pyscript.context
-
-## Display
-
-::: pyscript.display
-
-## Events
-
-::: pyscript.events
-
-## Fetch
-
-::: pyscript.fetch
-
-## FFI
-
-::: pyscript.ffi
-
-## Flatted
-
-::: pyscript.flatted
-
-## FS
-
-::: pyscript.fs
-
-## Media
-
-::: pyscript.media
-
-## Storage
-
-::: pyscript.storage
-
-## Util
-
-::: pyscript.util
-
-## Web
-
-::: pyscript.web
-
-## WebSocket
-
-::: pyscript.websocket
-
-## Workers
-
-::: pyscript.workers
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 19298e6..2e0e698 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -60,7 +60,15 @@ plugins:
css_dir: css
javascript_dir: js
canonical_version: null
- - mkdocstrings
+ - mkdocstrings:
+ default_handler: python
+ locale: en
+ handlers:
+ python:
+ options:
+ show_source: true
+ members_order: source
+ show_symbol_type_heading: true
nav:
- Home: index.md
@@ -83,7 +91,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
index 58e24bd..ecae1e4 100644
--- a/pyscript/__init__.py
+++ b/pyscript/__init__.py
@@ -1,45 +1,100 @@
"""
This is the main `pyscript` namespace. It provides the primary Pythonic API
-for users to interact with PyScript features sitting on top of 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.
+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.
-Some notes about the naming conventions and the relationship between various
-similar-but-different names found within this code base.
+The most important names provided by this namespace can be directly imported
+from `pyscript`, for example:
-`import pyscript`
+```python
+from pyscript import display, HTML, fetch, when, storage, WebSocket
+```
-This 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 file).
+The following names are available in the `pyscript` namespace:
-`import _pyscript`
+- `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.
-This is an internal module implemented in JS. It is used internally by
-the pyscript package, **end users should not use it directly**. For its
-implementation, grep for `interpreter.registerJsModule("_pyscript",
-...)` in `core.js`.
+If running in the main thread, the following additional names are available:
-`import js`
+- `create_named_worker`: Function to create a named Web Worker.
+- `workers`: Object to manage and interact with existing Web Workers.
-This is the JS `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.
+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.
-`import pyscript.context`
-This submodule abstracts away some of the differences between the main
-thread and a worker. 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).
+!!! Note
+ Some notes about the naming conventions and the relationship between
+ various similar-but-different names found within this code base.
-`from pyscript import window, document`
+ ```python
+ import pyscript
+ ```
-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.
+ 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`, 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
diff --git a/pyscript/context.py b/pyscript/context.py
index b5d8490..c685047 100644
--- a/pyscript/context.py
+++ b/pyscript/context.py
@@ -1,13 +1,16 @@
"""
Execution context management for PyScript.
-This module handles the differences between running in the main browser thread
-versus running in a Web Worker, providing a consistent API regardless of the
-execution context.
+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
- `pyscript.context.RUNNING_IN_WORKER`.
+ 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`.
@@ -17,16 +20,22 @@
- Provides access to the current display target via
`pyscript.context.display_target`.
-Main thread context:
-- `window` and `document` are available directly.
-- `PyWorker` can be created to spawn worker threads.
-- `sync` is not available (raises `NotSupported`).
+!!! warning
+
+ These are key differences between the main thread and worker contexts:
+
+ Main thread context:
-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.
+ - `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
@@ -37,14 +46,26 @@
from polyscript import js_modules
from pyscript.util import NotSupported
-# Detect execution context: True if running in a worker, False if main thread.
RUNNING_IN_WORKER = not hasattr(js, "document")
+"""Detect execution context: True if running in a worker, False if main thread."""
-# Parse and normalize configuration from polyscript.
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"
@@ -121,7 +142,6 @@ def __getattr__(self, field):
js.console.warn(sab_error_message)
window = NotSupported("pyscript.window", sab_error_message)
document = NotSupported("pyscript.document", sab_error_message)
- js_import = None
# Worker-specific utilities for main thread communication.
sync = polyscript.xworker.sync
@@ -135,9 +155,11 @@ def current_target():
else:
# Main thread context setup.
import _pyscript
- from _pyscript import PyWorker as _PyWorker, js_import
+ 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.
@@ -149,12 +171,13 @@ def PyWorker(url, **options):
```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
+ PyWorker **can only be created from the main thread**, not from
within another worker.
"""
return _PyWorker(url, to_js(options))
diff --git a/pyscript/display.py b/pyscript/display.py
index 0af530a..69efd5d 100644
--- a/pyscript/display.py
+++ b/pyscript/display.py
@@ -3,21 +3,22 @@
This module provides the `display()` function for rendering Python objects
in the web page. The function introspects objects to determine the appropriate
-MIME type and rendering method.
+[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)
+- `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.
-Object can provide a `_repr_mimebundle_` method to specify preferred formats
+Objects can provide a `_repr_mimebundle_` method to specify preferred formats
like this:
```python
@@ -28,9 +29,8 @@ def _repr_mimebundle_(self):
}
```
-Heavily inspired by IPython's rich display system. See:
-
-https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html
+Heavily inspired by
+[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html).
"""
import base64
@@ -95,7 +95,8 @@ class HTML:
display(HTML("Hello World "))
```
- Inspired by IPython.display.HTML.
+ Inspired by
+ [`IPython.display.HTML`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.HTML).
"""
def __init__(self, html):
diff --git a/pyscript/events.py b/pyscript/events.py
index 925cd60..dd4fd22 100644
--- a/pyscript/events.py
+++ b/pyscript/events.py
@@ -86,13 +86,14 @@ def remove_listener(self, *listeners):
def when(event_type, selector=None):
"""
- A decorator to handle DOM events or custom Event objects.
+ A decorator to handle DOM events or custom `Event` objects.
For DOM events, specify the `event_type` (e.g. `"click"`) and a `selector`
for target elements. For custom `Event` objects, just pass the `Event`
instance as the `event_type`. It's also possible to pass a list of `Event`
objects. The `selector` is required only for DOM events. It should be a
- CSS selector string, Element, ElementCollection, or list of DOM elements.
+ [CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors),
+ `Element`, `ElementCollection`, or list of DOM elements.
The decorated function can be either a regular function or an async
function. If the function accepts an argument, it will receive the event
diff --git a/pyscript/fetch.py b/pyscript/fetch.py
index 28cb4ad..97de453 100644
--- a/pyscript/fetch.py
+++ b/pyscript/fetch.py
@@ -1,8 +1,7 @@
"""
-A Pythonic wrapper around JavaScript's fetch API.
-
-This module provides a Python-friendly interface to the browser's fetch API,
-returning native Python data types and supported directly awaiting the promise
+This module provides a Python-friendly interface to the
+[browser's fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
+returning native Python data types and supporting directly awaiting the promise
and chaining method calls directly on the promise.
```python
@@ -11,7 +10,10 @@
# Pattern 1: Await the response, then extract data.
response = await fetch(url)
-data = await response.json()
+if response.ok:
+ data = await response.json()
+else:
+ raise NetworkError(f"Fetch failed: {response.status}")
# Pattern 2: Chain method calls directly on the promise.
data = await fetch(url).json()
@@ -160,8 +162,8 @@ def fetch(url, **options):
- `headers`: Dict of request headers.
- `body`: Request body (string, dict for JSON, etc.)
- See the MDN documentation for details:
- https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
+ See [this documentation](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)
+ for more details of these web standards.
The function returns a promise that resolves to a Response-like object
with Pythonic methods to extract data:
diff --git a/pyscript/ffi.py b/pyscript/ffi.py
index 1100277..2394862 100644
--- a/pyscript/ffi.py
+++ b/pyscript/ffi.py
@@ -1,9 +1,8 @@
"""
-Consistent Foreign Function Interface (FFI) utilities for PyScript.
-
-This module provides a unified FFI layer that works consistently across both
-Pyodide and MicroPython, and in worker or main thread contexts, abstracting
-away the differences in their JavaScript interop APIs.
+This module provides a unified Foreign Function Interface (FFI) layer that
+works consistently across both Pyodide and MicroPython, and in worker or main
+thread contexts, abstracting away the differences in their JavaScript interop
+APIs.
The following utilities work on both the main thread and in worker contexts:
@@ -18,10 +17,8 @@
- `gather`: Collect multiple values from worker contexts.
- `query`: Query objects in worker contexts.
-More details of the `direct`, `gather`, and `query` utilities can be found
-here:
-
-https://github.com/WebReflection/reflected-ffi?tab=readme-ov-file#remote-extra-utilities
+More details of the `direct`, `gather`, and `query` utilities
+[can be found here](https://github.com/WebReflection/reflected-ffi?tab=readme-ov-file#remote-extra-utilities).
"""
try:
@@ -77,16 +74,19 @@ def to_js(value, **kw):
"""
Convert Python objects to JavaScript objects.
- This ensures Python dicts become proper JavaScript objects rather
- than Maps, which is more intuitive for most use cases.
+ This ensures a Python `dict` becomes a
+ [proper JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
+ rather a JavaScript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map),
+ which is more intuitive for most use cases.
- Where required, the underlying to_js uses Object.fromEntries for dict
- conversion.
+ Where required, the underlying `to_js` uses `Object.fromEntries` for
+ `dict` conversion.
```python
from pyscript import ffi
import js
+
note = {
"body": "This is a notification",
"icon": "icon.png"
@@ -111,6 +111,7 @@ def is_none(value):
from pyscript import ffi
import js
+
val1 = None
val2 = js.null
val3 = 42
@@ -145,7 +146,8 @@ def is_none(value):
def assign(source, *args):
"""
- Merge JavaScript objects (like Object.assign).
+ Merge JavaScript objects (like
+ [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)).
Takes a target object and merges properties from one or more source
objects into it, returning the modified target.
diff --git a/pyscript/flatted.py b/pyscript/flatted.py
index 73fa82d..b001b36 100644
--- a/pyscript/flatted.py
+++ b/pyscript/flatted.py
@@ -1,10 +1,8 @@
"""
-Circular JSON parser for Python.
-
-This module is a Python implementation of the Flatted JavaScript library
-(https://www.npmjs.com/package/flatted), which provides a super light and
-fast way to serialize and deserialize JSON structures that contain circular
-references.
+This module is a Python implementation of the
+[Flatted JavaScript library](https://www.npmjs.com/package/flatted), which
+provides a light and fast way to serialize and deserialize JSON structures
+that contain circular references.
Standard JSON cannot handle circular references - attempting to serialize an
object that references itself will cause an error. Flatted solves this by
@@ -12,13 +10,15 @@
serialized and later reconstructed.
Common use cases:
-- Serializing complex object graphs with circular references
-- Working with DOM-like structures that contain parent/child references
-- Preserving object identity when serializing data structures
+
+- Serializing complex object graphs with circular references.
+- Working with DOM-like structures that contain parent/child references.
+- Preserving object identity when serializing data structures.
```python
from pyscript import flatted
+
# Create a circular structure.
obj = {"name": "parent"}
obj["self"] = obj # Circular reference!
@@ -157,6 +157,7 @@ def parse(value, *args, **kwargs):
```python
from pyscript import flatted
+
# Parse a Flatted JSON string.
json_string = '[{"name": "1", "self": "0"}, "parent"]'
obj = flatted.parse(json_string)
@@ -201,6 +202,7 @@ def stringify(value, *args, **kwargs):
```python
from pyscript import flatted
+
# Create an object with a circular reference.
parent = {"name": "parent", "children": []}
child = {"name": "child", "parent": parent}
diff --git a/pyscript/fs.py b/pyscript/fs.py
index 58cfc4d..ec31282 100644
--- a/pyscript/fs.py
+++ b/pyscript/fs.py
@@ -1,16 +1,12 @@
"""
-Filesystem mounting for Chromium-based browsers.
-
This module provides an API for mounting directories from the user's local
-filesystem into the browser's virtual filesystem. This allows Python code
-running in the browser to read and write files on the user's local machine.
-
-**Important:** This API only works in Chromium-based browsers (Chrome, Edge,
-Opera, Brave, etc.) that support the File System Access API.
+filesystem into the browser's virtual filesystem. This means Python code,
+running in the browser, can read and write files on the user's local machine.
-For technical details of the underlying Chromium based API, see:
-
-https://wicg.github.io/file-system-access/
+!!! warning
+ **This API only works in Chromium-based browsers** (Chrome, Edge,
+ Opera, Brave, etc.) that support the
+ [File System Access API](https://wicg.github.io/file-system-access/).
The module maintains a `mounted` dictionary that tracks all currently mounted
paths and their associated filesystem handles.
@@ -18,6 +14,7 @@
```python
from pyscript import fs, document, when
+
# Mount a local directory to the `/local` mount point in the browser's
# virtual filesystem (may prompt user for permission).
await fs.mount("/local")
@@ -52,8 +49,8 @@ async def handler(event):
from pyscript.context import sync as sync_with_worker
from polyscript import IDBMap
-# Global dictionary tracking mounted paths and their filesystem handles.
mounted = {}
+"""Global dictionary tracking mounted paths and their filesystem handles."""
async def _check_permission(details):
@@ -81,6 +78,7 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
```python
from pyscript import fs
+
# Basic mount with default settings.
await fs.mount("/local")
@@ -164,6 +162,7 @@ async def sync(path):
```python
from pyscript import fs
+
await fs.mount("/local")
# Make changes to files.
@@ -195,6 +194,7 @@ async def unmount(path):
```python
from pyscript import fs
+
await fs.mount("/local")
# ... work with files ...
await fs.unmount("/local")
@@ -203,7 +203,7 @@ async def unmount(path):
await fs.mount("/local", id="different-folder")
```
- This automatically calls sync() before unmounting to ensure no data
+ This automatically calls `sync()` before unmounting to ensure no data
is lost.
"""
if path not in mounted:
@@ -220,13 +220,14 @@ async def revoke(path, id="pyscript"):
`path` and `id` combination.
This removes the stored permission for accessing the user's local
- filesystem at the specified path and ID. Unlike unmount(), which only
- removes the mount point, revoke() also clears the permission so the
+ filesystem at the specified path and ID. Unlike `unmount()`, which only
+ removes the mount point, `revoke()` also clears the permission so the
user will be prompted again on next mount.
```python
from pyscript import fs
+
await fs.mount("/local", id="my-app")
# ... work with files ...
@@ -238,7 +239,7 @@ async def revoke(path, id="pyscript"):
```
After revoking, the user will need to grant permission again and
- select a directory when mount() is called next time.
+ select a directory when `mount()` is called next time.
"""
mount_key = f"{path}@{id}"
diff --git a/pyscript/media.py b/pyscript/media.py
index 716331f..c541b14 100644
--- a/pyscript/media.py
+++ b/pyscript/media.py
@@ -1,8 +1,7 @@
"""
-Media device access for PyScript.
-
-This module provides classes and functions for interacting with media devices
-and streams in the browser, enabling you to work with cameras, microphones,
+This module provides classes and functions for interacting with
+[media devices and streams](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API)
+in the browser, enabling you to work with cameras, microphones,
and other media input/output devices directly from Python.
Use this module for:
@@ -12,11 +11,11 @@
- Enumerating available media devices.
- Applying constraints to media streams (resolution, frame rate, etc.).
-
```python
from pyscript import document
from pyscript.media import Device, list_devices
+
# Get a video stream from the default camera.
stream = await Device.request_stream(video=True)
@@ -42,16 +41,18 @@ class Device:
"""
Represents a media input or output device.
- This class wraps a browser MediaDeviceInfo object, providing Pythonic
- access to device properties like ID, label, and kind (audio/video
- input/output).
+ This class wraps a browser
+ [MediaDeviceInfo object](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo),
+ providing Pythonic access to device properties like `ID`, `label`, and
+ `kind` (audio/video, input/output).
- Devices are typically obtained via `list_devices()` rather than
- constructed directly.
+ Devices are typically obtained via the `list_devices()` function in this
+ module, rather than constructed directly.
```python
from pyscript.media import list_devices
+
# Get all available devices.
devices = await list_devices()
@@ -75,7 +76,7 @@ def id(self):
"""
Unique identifier for this device.
- This ID persists across sessions but is reset when the user clears
+ This `ID` persists across sessions but is reset when the user clears
cookies. It's unique to the origin of the calling application.
"""
return self._device_info.deviceId
@@ -86,14 +87,14 @@ def group(self):
Group identifier for related devices.
Devices belonging to the same physical device (e.g., a monitor with
- both a camera and microphone) share the same group ID.
+ both a camera and microphone) share the same `group ID`.
"""
return self._device_info.groupId
@property
def kind(self):
"""
- Device type: "videoinput", "audioinput", or "audiooutput".
+ Device type: `"videoinput"`, `"audioinput"`, or `"audiooutput"`.
"""
return self._device_info.kind
@@ -102,7 +103,7 @@ def label(self):
"""
Human-readable description of the device.
- Example: "External USB Webcam" or "Built-in Microphone".
+ Example: `"External USB Webcam"` or `"Built-in Microphone"`.
"""
return self._device_info.label
@@ -110,7 +111,7 @@ def __getitem__(self, key):
"""
Support bracket notation for JavaScript interop.
- Allows accessing properties via device["id"] syntax. Necessary
+ Allows accessing properties via `device["id"]` syntax. Necessary
when Device instances are proxied to JavaScript.
"""
return getattr(self, key)
@@ -127,14 +128,14 @@ async def request_stream(cls, audio=False, video=True):
Simple boolean constraints for `audio` and `video` can be used to
request default devices. More complex constraints can be specified as
- dictionaries conforming to the MediaTrackConstraints interface. See:
-
- https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
+ dictionaries conforming to
+ [the MediaTrackConstraints interface](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).
```python
from pyscript import document
from pyscript.media import Device
+
# Get default video stream.
stream = await Device.request_stream()
@@ -169,10 +170,11 @@ async def request_stream(cls, audio=False, video=True):
@classmethod
async def load(cls, audio=False, video=True):
"""
- Deprecated: Use request_stream() instead.
+ !!! warning
+ **Deprecated: Use `request_stream()` instead.**
- This method is retained for backwards compatibility but will be
- removed in a future release. Please use request_stream() instead.
+ This method is retained for backwards compatibility but will be
+ removed in a future release. Please use `request_stream()` instead.
"""
return await cls.request_stream(audio=audio, video=video)
@@ -183,6 +185,7 @@ async def get_stream(self):
```python
from pyscript.media import list_devices
+
# List all devices.
devices = await list_devices()
@@ -210,14 +213,13 @@ async def get_stream(self):
async def list_devices():
"""
- List all available media input and output devices.
-
Returns a list of all media devices currently available to the browser,
such as microphones, cameras, and speakers.
```python
from pyscript.media import list_devices
+
# Get all devices.
devices = await list_devices()
@@ -232,13 +234,14 @@ async def list_devices():
```
The returned list will omit devices that are blocked by the document
- Permission Policy (microphone, camera, speaker-selection) or for
+ [Permission Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy)
+ (microphone, camera, speaker-selection) or for
which the user has not granted explicit permission.
For security and privacy, device labels may be empty strings until
- permission is granted. See:
-
- https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
+ permission is granted. See
+ [this document](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices)
+ for more information about this web standard.
"""
device_infos = await window.navigator.mediaDevices.enumerateDevices()
return [Device(device_info) for device_info in device_infos]
diff --git a/pyscript/storage.py b/pyscript/storage.py
index 3a13f7e..ca2fe6c 100644
--- a/pyscript/storage.py
+++ b/pyscript/storage.py
@@ -1,9 +1,8 @@
"""
-Persistent browser storage with a Pythonic dict-like interface.
-
-This module wraps the browser's IndexedDB persistent storage to provide a
-familiar Python dictionary API. Data is automatically serialized and
-persisted, surviving page reloads and browser restarts.
+This module wraps the browser's
+[IndexedDB persistent storage](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
+to provide a familiar Python dictionary API. Data is automatically
+serialized and persisted, surviving page reloads and browser restarts.
Storage is persistent per origin (domain), isolated between different sites
for security. Browsers typically allow each origin to store up to 10-60% of
@@ -11,7 +10,7 @@
What this module provides:
-- Dict-like API (get, set, delete, iterate).
+- A `dict`-like API (get, set, delete, iterate).
- Automatic serialization of common Python types.
- Background persistence with optional explicit `sync()`.
- Support for custom `Storage` subclasses.
@@ -19,6 +18,7 @@
```python
from pyscript import storage
+
# Create or open a named storage.
my_data = await storage("user-preferences")
@@ -35,16 +35,17 @@
theme = my_data.get("theme", "light")
```
-Common types are automatically serialized: bool, int, float, str, None,
-list, dict, tuple. Binary data (bytearray, memoryview) can be stored as
+Common types are automatically serialized: `bool`, `int`, `float`, `str`, `None`,
+`list`, `dict`, `tuple`. Binary data (`bytearray`, `memoryview`) can be stored as
single values but not nested in structures.
Tuples are deserialized as lists due to IndexedDB limitations.
-Browsers typically allow 10-60% of total disk space per origin. Chrome
-and Edge allow up to 60%, Firefox up to 10 GiB (or 10% of disk, whichever
-is smaller). Safari varies by app type. These limits are unlikely to be
-reached in typical usage.
+!!! info
+ Browsers typically allow 10-60% of total disk space per origin. Chrome
+ and Edge allow up to 60%, Firefox up to 10 GiB (or 10% of disk, whichever
+ is smaller). Safari varies by app type. These limits are unlikely to be
+ reached in typical usage.
"""
from polyscript import storage as _polyscript_storage
@@ -97,7 +98,7 @@ def _convert_from_idb(value):
class Storage(dict):
"""
- A persistent dictionary backed by browser IndexedDB.
+ A persistent dictionary backed by the browser's IndexedDB.
This class provides a dict-like interface with automatic persistence.
Changes are queued for background writing, with optional explicit
@@ -108,6 +109,7 @@ class Storage(dict):
```python
from pyscript import storage
+
# Open a storage.
prefs = await storage("preferences")
@@ -128,6 +130,7 @@ class Storage(dict):
```python
from pyscript import storage, Storage, window
+
class LoggingStorage(Storage):
def __setitem__(self, key, value):
window.console.log(f"Setting {key} = {value}")
@@ -173,7 +176,7 @@ def clear(self):
"""
Remove all items from storage.
- The clear operation is queued for persistence. Use `sync()` to ensure
+ The `clear()` operation is queued for persistence. Use `sync()` to ensure
immediate completion.
"""
self._store.clear()
@@ -184,7 +187,7 @@ async def sync(self):
Force immediate synchronization to IndexedDB.
By default, storage operations are queued and written asynchronously.
- Call `sync()` when you need to guarantee data is persisted immediately,
+ Call `sync()` when you need to guarantee changes are persisted immediately,
such as before critical operations or page unload.
```python
@@ -210,13 +213,14 @@ async def storage(name="", storage_class=Storage):
If the storage doesn't exist, it will be created. If it does exist,
its current contents will be loaded.
- This function returns a Storage instance (or custom subclass instance)
- acting as a persistent dictionary. A ValueError is raised if `name` is
+ This function returns a `Storage` instance (or custom subclass instance)
+ acting as a persistent dictionary. A `ValueError` is raised if `name` is
empty or not provided.
```python
from pyscript import storage
+
# Basic usage.
user_data = await storage("user-profile")
user_data["name"] = "Alice"
@@ -236,7 +240,7 @@ def __setitem__(self, key, value):
validated = await storage("validated-data", ValidatingStorage)
```
- Storage names are automatically prefixed with "@pyscript/" to
+ Storage names are automatically prefixed with `"@pyscript/"` to
namespace them within IndexedDB.
"""
if not name:
diff --git a/pyscript/util.py b/pyscript/util.py
index bbc1c35..4f45dcb 100644
--- a/pyscript/util.py
+++ b/pyscript/util.py
@@ -1,6 +1,4 @@
"""
-Utility functions for PyScript.
-
This module contains general-purpose utility functions that don't fit into
more specific modules. These utilities handle cross-platform compatibility
between Pyodide and MicroPython, feature detection, and common type
@@ -20,7 +18,7 @@
def as_bytearray(buffer):
"""
- Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
+ Given a JavaScript `ArrayBuffer`, convert it to a Python `bytearray` in a
MicroPython friendly manner.
"""
ui8a = js.Uint8Array.new(buffer)
@@ -57,13 +55,16 @@ def __call__(self, *args):
def is_awaitable(obj):
"""
Returns a boolean indication if the passed in obj is an awaitable
- function. (MicroPython treats awaitables as generator functions, and if
- the object is a closure containing an async function we need to work
- carefully.)
+ function. This is interpreter agnostic.
+
+ !!! info
+ MicroPython treats awaitables as generator functions, and if
+ the object is a closure containing an async function or a bound method
+ we need to work carefully.
"""
from pyscript import config
- if config["type"] == "mpy": # Is MicroPython?
+ if config["type"] == "mpy":
# MicroPython doesn't appear to have a way to determine if a closure is
# an async function except via the repr. This is a bit hacky.
r = repr(obj)
@@ -72,6 +73,7 @@ def is_awaitable(obj):
# Same applies to bound methods.
if "" in r:
return True
+ # In MicroPython, generator functions are awaitable.
return inspect.isgeneratorfunction(obj)
return inspect.iscoroutinefunction(obj)
diff --git a/pyscript/web.py b/pyscript/web.py
index 43d8bd4..4b93261 100644
--- a/pyscript/web.py
+++ b/pyscript/web.py
@@ -1,13 +1,16 @@
"""
A lightweight Pythonic interface to the DOM and HTML elements that helps you
-to interact with web pages, making it easy to find, create, manipulate, and
+interact with web pages, making it easy to find, create, manipulate, and
compose HTML elements from Python.
+Highlights include:
+
Use the `page` object to find elements on the current page:
```python
from pyscript import web
+
# Find by CSS selector (returns an ElementCollection).
divs = web.page.find("div")
buttons = web.page.find(".button-class")
@@ -67,7 +70,7 @@
)
```
-An element's CSS classes behave like Python sets:
+An element's CSS classes behave like a Python `set`:
```python
# Add and remove classes
@@ -86,7 +89,7 @@
element.classes.discard("maybe-not-there")
```
-An element's styles behave like Python dictionaries:
+An element's styles behave like a Python `dict`:
```python
# Set individual styles.
@@ -102,7 +105,7 @@
print(f"Color is {element.style['color']}")
```
-Update multiple elements at once via an ElementCollection:
+Update multiple elements at once via an `ElementCollection`:
```python
# Find multiple elements (returns an ElementCollection).
@@ -177,7 +180,7 @@ def another_handler(event):
button = web.button("Click", on_click=handle_click)
```
-All Element instances provide direct access to the underlying DOM element
+All `Element` instances provide direct access to the underlying DOM element
via attribute delegation:
```python
@@ -186,7 +189,7 @@ def another_handler(event):
element.focus()
element.blur()
-# But we do have a convenience method for scrolling into view.
+# But we do have a historic convenience method for scrolling into view.
element.show_me() # Calls scrollIntoView()
# Access the raw DOM element when needed for special cases.
@@ -235,13 +238,14 @@ def _find_and_wrap(dom_node, selector):
class Element:
"""
- The base class for all HTML elements.
+ The base class for all [HTML elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements).
Provides a Pythonic interface to DOM elements with support for attributes,
events, styles, classes, and DOM manipulation. It can create new elements
or wrap existing DOM elements.
- Elements are typically created using the tag-specific classes:
+ Elements are typically created using the tag-specific classes found
+ within this namespace (e.g. `web.div`, `web.span`, `web.button`):
```python
from pyscript import web
@@ -262,13 +266,22 @@ class Element:
)
```
+ !!! info
+
+ Some elements have an underscore suffix in their class names (e.g.
+ `select_`, `input_`).
+
+ This is to avoid clashes with Python keywords. The underscore is removed
+ when determining the actual HTML tag name.
+
Wrap existing DOM elements found on the page:
```python
- # Find and wrap an element.
- existing = web.page.find("#my-element")[0]
+ # Find and wrap an element by CSS selector.
+ existing = web.page.find(".my_class")[0]
- # Or, better, just use direct ID lookup.
+ # Or, better, just use direct ID lookup (with or without the
+ # leading '#').
existing = web.page["my-element"]
```
@@ -290,7 +303,7 @@ class Element:
div.textContent = "Plain text"
```
- CSS classes are managed through a set-like interface:
+ CSS classes are managed through a `set`-like interface:
```python
# Add classes.
@@ -310,7 +323,7 @@ class Element:
print(cls)
```
- Explicit CSS styles are managed through a dict-like interface:
+ Explicit CSS styles are managed through a `dict`-like interface:
```python
# Set styles using CSS property names (hyphenated).
@@ -380,14 +393,17 @@ def handle_click(event):
)
```
- **Some HTML attributes clash with Python keywords and use trailing
- underscores**:
+ !!! warning
+ **Some HTML attributes clash with Python keywords and use trailing
+ underscores**.
+
+ Use `for_` instead of `for`, and `class_` instead of `class`.
```python
# The 'for' attribute (on labels)
label = web.label("Username", for_="username-input")
- # The 'class' attribute (though 'classes' is preferred)
+ # The 'class' attribute (although 'classes' is preferred)
div.class_ = "my-class"
```
@@ -522,7 +538,8 @@ def __setattr__(self, name, value):
Set an attribute on the element.
Private attributes (starting with `_`) are set on the Python object.
- Public attributes are set on the underlying DOM element.
+ Public attributes are set on the underlying DOM element. Attributes
+ starting with `on_` are treated as events.
"""
if name.startswith("_"):
super().__setattr__(name, value)
@@ -578,7 +595,7 @@ def children(self):
@property
def classes(self):
"""
- Return the element's CSS classes as a set-like object.
+ Return the element's CSS classes as a `set`-like `Classes` object.
Supports set operations: `add`, `remove`, `discard`, `clear`.
Check membership with `in`, iterate with `for`, get length with `len()`.
@@ -596,9 +613,10 @@ def classes(self):
@property
def style(self):
"""
- Return the element's CSS styles as a dict-like object.
+ Return the element's CSS styles as a `dict`-like `Style` object.
- Access using dict-style syntax with CSS property names (hyphenated).
+ Access using `dict`-style syntax with standard
+ [CSS property names (hyphenated)](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference).
```python
element.style["background-color"] = "red"
@@ -657,7 +675,8 @@ def clone(self, clone_id=None):
def find(self, selector):
"""
- Find all descendant elements matching the CSS selector.
+ Find all descendant elements matching the
+ [CSS `selector`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors).
Returns an `ElementCollection` (possibly empty).
@@ -698,10 +717,8 @@ def update(self, classes=None, style=None, **kwargs):
class Classes(set):
"""
- A set of CSS class names that syncs with the DOM.
-
- Behaves like a Python set with changes automatically reflected in the
- element's classList.
+ Behaves like a Python `set` with changes automatically reflected in the
+ element's `classList`.
```python
# Add and remove classes.
@@ -723,6 +740,7 @@ class Classes(set):
"""
def __init__(self, element):
+ """Initialise the Classes set for the given element."""
self._class_list = element._dom_element.classList
super().__init__(self._class_list)
@@ -766,10 +784,8 @@ def clear(self):
class Style(dict):
"""
- A dictionary of CSS styles that syncs with the DOM.
-
- Behaves like a Python dict with changes automatically reflected in the
- element's style attribute.
+ Behaves like a Python `dict` with changes automatically reflected in the
+ element's `style` attribute.
```python
# Set and get styles using CSS property names (hyphenated).
@@ -790,6 +806,7 @@ class Style(dict):
"""
def __init__(self, element):
+ """Initialise the Style dict for the given element."""
self._style = element._dom_element.style
super().__init__()
@@ -808,7 +825,7 @@ class HasOptions:
"""
Mixin for elements with options (`datalist`, `optgroup`, `select`).
- Provides an options property that returns an `Options` instance. Used
+ Provides an `options` property that returns an `Options` instance. Used
in conjunction with the `Options` class.
```python
@@ -833,7 +850,7 @@ class HasOptions:
@property
def options(self):
- """Return this element's options as an Options instance."""
+ """Return this element's options as an `Options` instance."""
if not hasattr(self, "_options"):
self._options = Options(self)
return self._options
@@ -933,7 +950,7 @@ def clear(self):
def remove(self, index):
"""
- Remove the option at the specified index.
+ Remove the option at the specified `index`.
"""
self._element._dom_element.remove(index)
@@ -978,8 +995,9 @@ def __init__(
Create a container element with optional `children`.
Children can be passed as positional `*args` or via the `children`
- keyword argument. String children are inserted as HTML. The `style`,
- `classes`, and `**kwargs` are passed to the base `Element` initializer.
+ keyword argument. String children are inserted as unescaped HTML. The
+ `style`, `classes`, and `**kwargs` are passed to the base `Element`
+ initializer.
"""
super().__init__(
dom_element=dom_element, style=style, classes=classes, **kwargs
@@ -997,7 +1015,7 @@ def __iter__(self):
class ElementCollection:
"""
- A collection of Element instances with list-like operations.
+ A collection of Element instances with `list`-like operations.
Supports iteration, indexing, slicing, and finding descendants.
For bulk operations, iterate over the collection explicitly or use
@@ -1022,7 +1040,7 @@ class ElementCollection:
item.innerHTML = "Updated"
item.classes.add("processed")
- # Bulk update all elements.
+ # Bulk update all contained elements.
items.update_all(innerHTML="Hello", className="updated")
# Find matches within the collection.
@@ -1036,7 +1054,7 @@ class ElementCollection:
@classmethod
def wrap_dom_elements(cls, dom_elements):
"""
- Wrap an iterable of DOM elements in an ElementCollection.
+ Wrap an iterable of DOM elements in an `ElementCollection`.
"""
return cls(
[Element.wrap_dom_element(dom_element) for dom_element in dom_elements]
@@ -1098,13 +1116,14 @@ def __repr__(self):
@property
def elements(self):
"""
- Return the underlying list of elements.
+ Return the underlying `list` of elements.
"""
return self._elements
def find(self, selector):
"""
- Find all descendants matching the CSS selector.
+ Find all descendants matching the
+ [CSS `selector`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors).
Searches within all elements in the collection.
@@ -1138,9 +1157,9 @@ def update_all(self, **kwargs):
class canvas(ContainerElement):
"""
- HTML canvas element with drawing and download capabilities.
-
- Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas
+ A bespoke
+ [HTML canvas element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)
+ with Pythonic drawing and download capabilities.
"""
def download(self, filename="snapped.png"):
@@ -1177,9 +1196,9 @@ def draw(self, what, width=None, height=None):
class video(ContainerElement):
"""
- HTML video element with snapshot capability.
-
- Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video
+ A bespoke
+ [HTML video element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)
+ with Pythonic snapshot capability (to render an image to a canvas).
"""
def snap(self, to=None, width=None, height=None):
@@ -1262,6 +1281,10 @@ class select(ContainerElement, HasOptions):
"var",
"wbr",
]
+"""
+Container elements that can have children. Each becomes a class in the
+`pyscript.web` namespace and corresponds to an HTML tag.
+"""
# fmt: on
# Void elements that cannot have children.
@@ -1278,6 +1301,10 @@ class select(ContainerElement, HasOptions):
"source",
"track",
]
+"""
+Void elements that cannot have children. Each becomes a class in the
+`pyscript.web` namespace and corresponds to an HTML tag.
+"""
def _create_element_classes():
@@ -1321,8 +1348,8 @@ class Page:
"""
Represents the current web page.
- Provides access to the document's html, head, and body elements, plus
- convenience methods for finding elements and appending to the body.
+ Provides access to the document's `html`, `head`, and `body` elements,
+ plus convenience methods for finding elements and appending to the body.
```python
from pyscript import web
@@ -1372,20 +1399,20 @@ def __getitem__(self, key):
@property
def title(self):
"""
- Get the page title.
+ Get the page `title`.
"""
return document.title
@title.setter
def title(self, value):
"""
- Set the page title.
+ Set the page `title`.
"""
document.title = value
def append(self, *items):
"""
- Append items to the page body.
+ Append items to the page `body`.
Shortcut for `page.body.append(*items)`.
"""
@@ -1393,7 +1420,8 @@ def append(self, *items):
def find(self, selector):
"""
- Find all elements matching the CSS selector.
+ Find all elements matching the
+ [CSS `selector`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors).
Returns an `ElementCollection` of matching elements.
@@ -1408,3 +1436,4 @@ def find(self, selector):
page = Page()
+"""A reference to the current web page. An instance of the `Page` class."""
diff --git a/pyscript/websocket.py b/pyscript/websocket.py
index 5d9874c..9fbd227 100644
--- a/pyscript/websocket.py
+++ b/pyscript/websocket.py
@@ -1,7 +1,6 @@
"""
-WebSocket support for PyScript.
-
-This module provides a Pythonic wrapper around the browser's WebSocket API,
+This module provides a Pythonic wrapper around the browser's
+[WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket),
enabling two-way communication with WebSocket servers.
Use this for real-time applications:
@@ -15,9 +14,9 @@
- Naming deliberately follows the JavaScript WebSocket API closely for
familiarity.
-See the Python docs for an explanation of memoryview:
+See the Python docs for
+[an explanation of memoryview](https://docs.python.org/3/library/stdtypes.html#memoryview).
-https://docs.python.org/3/library/stdtypes.html#memoryview
```python
from pyscript import WebSocket
@@ -38,9 +37,6 @@ def on_close(event):
ws.onmessage = on_message
ws.onclose = on_close
```
-
-For more information about the underlying WebSocket API, see:
-https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
"""
import js
@@ -73,7 +69,8 @@ async def async_wrapper(event):
class WebSocketEvent:
"""
- A read-only wrapper for WebSocket event objects.
+ A read-only wrapper for
+ [WebSocket event objects](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent).
This class wraps browser WebSocket events and provides convenient access
to event properties. It handles the conversion of binary data from
@@ -105,7 +102,7 @@ def __getattr__(self, attr):
Get an attribute `attr` from the underlying event object.
Handles special conversion of binary data from JavaScript typed
- arrays to Python memoryview objects.
+ arrays to Python `memoryview` objects.
"""
value = getattr(self._event, attr)
if attr == "data" and not isinstance(value, str):
@@ -124,11 +121,10 @@ class WebSocket:
handling communication with WebSocket servers. It supports both text and
binary data transmission.
- It's possible to access the underlying WebSocket methods and properties
- directly if needed. However, the wrapper provides a more Pythonic API.
-
- If you need to work with the raw JavaScript WebSocket instance, you can
- access it via the `_js_websocket` attribute.
+ Access the underlying WebSocket methods and properties directly if needed.
+ However, the wrapper provides a more Pythonic API. If you need to work
+ with the raw JavaScript WebSocket instance, you can access it via the
+ `_js_websocket` attribute.
Using textual (`str`) data:
@@ -169,7 +165,8 @@ def handle_message(event):
ws.send(data)
```
- See: https://docs.python.org/3/library/stdtypes.html#memoryview
+ Read more about Python's
+ [`memoryview` here](https://docs.python.org/3/library/stdtypes.html#memoryview).
"""
# WebSocket ready state constants.
@@ -180,15 +177,14 @@ def handle_message(event):
def __init__(self, url, protocols=None, **handlers):
"""
- Create a new WebSocket connection from the given `url` (ws:// or
- wss://). Optionally specify `protocols` (a string or a list of
- protocol strings) and event handlers (onopen, onmessage, etc.) as
+ Create a new WebSocket connection from the given `url` (`ws://` or
+ `wss://`). Optionally specify `protocols` (a string or a list of
+ protocol strings) and event handlers (`onopen`, `onmessage`, etc.) as
keyword arguments.
- These arguments and naming conventions mirror those of the underlying
- JavaScript WebSocket API for familiarity.
-
- https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
+ These arguments and naming conventions mirror those of the
+ [underlying JavaScript WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+ for familiarity.
If you need access to the underlying JavaScript WebSocket instance,
you can get it via the `_js_websocket` attribute.
@@ -230,7 +226,7 @@ def __getattr__(self, attr):
Get an attribute `attr` from the underlying WebSocket.
This allows transparent access to WebSocket properties like
- readyState, url, bufferedAmount, etc.
+ `readyState`, `url`, `bufferedAmount`, etc.
"""
return getattr(self._js_websocket, attr)
@@ -238,7 +234,7 @@ def __setattr__(self, attr, value):
"""
Set an attribute `attr` on the WebSocket to the given `value`.
- Event handler attributes (onopen, onmessage, etc.) are specially
+ Event handler attributes (`onopen`, `onmessage`, etc.) are specially
handled to create proper proxies. Other attributes are set on the
underlying WebSocket directly.
"""
@@ -249,10 +245,10 @@ def __setattr__(self, attr, value):
def send(self, data):
"""
- Send data through the WebSocket.
+ Send `data` through the WebSocket.
- Accepts both text (str) and binary data (bytes, bytearray, etc.).
- Binary data is automatically converted to a JavaScript Uint8Array.
+ Accepts both text (`str`) and binary data (`bytes`, `bytearray`, etc.).
+ Binary data is automatically converted to a JavaScript `Uint8Array`.
```python
# Send text.
@@ -263,7 +259,9 @@ def send(self, data):
ws.send(bytearray([5, 6, 7, 8]))
```
- The WebSocket **must be in the OPEN state to send data**.
+ !!! warning
+
+ The WebSocket **must be in the OPEN state to send data**.
"""
if isinstance(data, str):
self._js_websocket.send(data)
@@ -275,8 +273,8 @@ def send(self, data):
def close(self, code=None, reason=None):
"""
- Close the WebSocket connection. Optionally specify a `code` (integer)
- and a `reason` (string) for closing the connection.
+ Close the WebSocket connection. Optionally specify a `code` (`int`)
+ and a `reason` (`str`) for closing the connection.
```python
# Normal close.
@@ -286,9 +284,8 @@ def close(self, code=None, reason=None):
ws.close(code=1000, reason="Task completed")
```
- Usage and values for `code` and `reasons` are explained here:
-
- https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close
+ Usage and values for `code` and `reasons`
+ [are explained here](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close).
"""
if code and reason:
self._js_websocket.close(code, reason)
diff --git a/pyscript/workers.py b/pyscript/workers.py
index 4a104ba..5e23732 100644
--- a/pyscript/workers.py
+++ b/pyscript/workers.py
@@ -1,8 +1,8 @@
"""
-Worker management for PyScript.
-
-This module provides access to named web workers defined in script tags, and
-utilities for dynamically creating workers from Python code.
+This module provides access to named
+[web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
+defined in `
` now looks like:
This fragment of HTML contains the application's header (``), some
instructions between the ` ` tags, an ` ` box for the English text, and
-a `` to click to generate the translation. Towards the end there's a
-`` which will contain the resulting pirate speak as the
-application's output.
-
-There's something strange about the `
` tag: it has a `py-click`
-attribute with the value `translate_english`. This is, in fact, the name of a
-Python function we'll run whenever the button is clicked. Such `py-*` style
-attributes are [built into PyScript](api.md#html-attributes).
+a `` to click to generate the translation. Notice the button has an `id`
+attribute (`id="translate-button"`) which we'll use to attach a Python function
+to its `click` event. Towards the end there's a `` which will
+contain the resulting pirate speak as the application's output.
We put all this together in the `script` tag at the end of the ``. This
-tells the browser we're using PyScript (`type="py"`), and where PyScript
+tells the browser the script is using PyScript (`type="py"`), and where PyScript
should find the Python source code (`src="./main.py"`). Finally, we indicate
where PyScript should find the configuration (`config="./pyscript.json"`).
@@ -175,7 +172,7 @@ In the end, our HTML should look like this:
Polyglot 🦜 💬 🇬🇧 ➡️ 🏴☠️
Translate English into Pirate speak...
-
Translate
+
Translate
@@ -183,22 +180,26 @@ In the end, our HTML should look like this:
```
But this only defines _how_ the user interface should look. To define its
-behaviour we need to write some Python. Specifically, we need to define the
-`translate_english` function, used when the button is clicked.
+behaviour we need to write some Python. Specifically, we need to attach a
+function to the button's click event.
-### main.py
+### `main.py`
The behaviour of the application is defined in `main.py`. It looks like this:
-``` python linenums="1" title="main.py"
+```python linenums="1" title="main.py"
import arrr
-from pyscript import document
+from pyscript import web, when
+@when("click", "#translate-button")
def translate_english(event):
- input_text = document.querySelector("#english")
+ """
+ Translate English text to Pirate speak.
+ """
+ input_text = web.page["english"]
english = input_text.value
- output_div = document.querySelector("#output")
+ output_div = web.page["output"]
output_div.innerText = arrr.translate(english)
```
@@ -207,33 +208,52 @@ It's not very complicated Python code.
On line 1 the `arrr` module is imported so we can do the actual English to
Pirate translation. If we hadn't told PyScript to download the `arrr` module
in our `pyscript.json` configuration file, this line would cause an error.
-PyScript has ensured our environment is set up with the expected `arrr` module.
-
-Line 2 imports the `document` object. The `document` allows us to reach into
-the things on the web page defined in `index.html`.
-
-Finally, on line 5 the `translate_english` function is defined.
-
-The `translate_english` function takes a single parameter called
-`event`. This represents the user's click of the button (but which we don't
-actually use).
-
-Inside the body of the function we first get a reference to the `input`
-element with the [`document.querySelector` function](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
-that takes `#english` as its
-parameter (indicating we want the element with the id "english"). We assign the
-result to `input_text`, then extract the user's `english` from the
-`input_text`'s `value`. Next, we get a reference called `output_div` that
-points to the `div` element with the id "output". Finally, we assign the
-`innerText` of the `output_div` to the result of calling
+PyScript has ensured our environment is set up with the expected `arrr` module
+before our Python code is evaluated.
+
+Line 2 imports the `web` module and the `when` decorator from `pyscript`. The
+`web` module provides a Pythonic way to interact with the web page, while
+`when` is used to easily attach Python functions to browser events.
+
+On line 5 we use the `@when` decorator to attach our function to the button's
+click event. The decorator takes two arguments: the event type (`"click"`) and
+a CSS selector identifying the element (via the id `"#translate-button"`).
+This is PyScript's idiomatic way to handle events - much more Pythonic than
+HTML attributes.
+
+The `translate_english` function is defined on line 6. It takes a single
+parameter called `event`, which represents the browser event that triggered the
+function (in this case, the user's click on the button).
+
+Inside the body of the function we use `web.page["english"]` to get a reference
+to the `
` element with the id "english". The `web.page` object
+represents the current web page, and using the square bracket notation
+(`web.page["element-id"]`) is PyScript's Pythonic way to find elements by their
+unique id. We assign the result to `input_text`, then extract the user's
+`english` text from the `input_text`'s `value` attribute.
+
+Next, we get a reference called `output_div` that points to the `
` element
+with the id "output" using the same `web.page["output"]` pattern. Finally, we
+assign the `innerText` of the `output_div` to the result of calling
[`arrr.translate`](https://arrr.readthedocs.io/en/latest/#arrr.translate)
-(to actually translate the `english` to something piratical).
+to actually translate the `english` to something piratical.
That's it!
+<<<<<<< HEAD
+## Editing your app
+=======
+!!! info "Alternative: JavaScript-style DOM access"
+>>>>>>> 2bc3162 (First draft of comprehensive user-guide re-write. TODO: check example apps again.)
+
+ PyScript also provides direct access to the browser's JavaScript APIs. If
+ you're already familiar with JavaScript, you can use `document.querySelector`
+ and other standard
+ [DOM methods](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model).
+
## Editing your app
-If you use an IDE (like VSCode or PyCharm) then you'll probably want them to
+If you use an IDE (like VSCode or PyCharm) then you'll probably want it to
auto-suggest and introspect aspects of the Python code you're writing. The
problem is that the `pyscript` namespace *we provide* isn't installed anywhere
(because it's in your browser, not your IDE's context) so such information
@@ -247,7 +267,7 @@ You should clone the linked-to repository and configure your IDE to consume the
stub files.
For example, let's say you
-[cloned the repository](https://github.com/pyscript/pyscript-stubs) into:
+[cloned the repository](https://github.com/pyscript/pyscript-stubs) into
`~/src/stubs/pyscript-stubs`, then in VSCode, you'd create, in your PyScript
project, a file called `.vscode/settings.json` and add the following:
@@ -257,9 +277,9 @@ project, a file called `.vscode/settings.json` and add the following:
}
```
-Then restart the Python language server in VSCode (Press `Ctrl+Shift+P` (or
-`Cmd+Shift+P` on Mac) to open the Command Palette and type:
-`Python: Restart Language Server`.
+Then restart the Python language server in VSCode (press `Ctrl+Shift+P`, or
+`Cmd+Shift+P` on Mac, to open the Command Palette and type
+`Python: Restart Language Server`).
!!! note
@@ -268,35 +288,28 @@ Then restart the Python language server in VSCode (Press `Ctrl+Shift+P` (or
## Sharing your app
-### PyScript.com
-
-If you're using [pyscript.com](https://pyscript.com), you should save all your files
-and click the "run" button. Assuming you've copied the code properly, you
-should have a fine old time using "Polyglot 🦜" to translate English to
-Pirate-ish.
-
-Alternatively, [click here to see a working example of this app](https://ntoll.pyscriptapps.com/piratical/v5/).
-Notice that the bottom right hand corner contains a link to view the code on
-[pyscript.com](https://pyscript.com). Why not explore the code, copy it to your own
-account and change it to your satisfaction?
-
### From a web server
-Just host the three files (`pyscript.json`, `index.html`
-and `main.py`) in the same directory on a static web server somewhere.
+To share your PyScript application, host the three files (`pyscript.json`,
+`index.html` and `main.py`) in the same directory on a static web server.
-Clearly, we recommend you use [pyscript.com](https://pyscript.com) for this, but any
-static web host will do (for example,
+Any static web host will work (for example,
[GitHub Pages](https://pages.github.com/),
[Amazon's S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html),
[Google Cloud](https://cloud.google.com/storage/docs/hosting-static-website) or
[Microsoft's Azure](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website)).
-## Run PyScript Offline
+### Using PyScript.com
+
+If you're using [pyscript.com](https://pyscript.com) for development, you can also
+deploy directly from there. Save all your files and click the "run" button to test
+your application. The platform provides hosting and generates a shareable link for
+your app.
+
+## Run PyScript offline
To run PyScript offline, without the need of a CDN or internet connection, read
-the [Run PyScript Offline](user-guide/offline.md) section of the user
-guide.
+the [offline guide](user-guide/offline.md) section of the user guide.
We also provide an `offline.zip` file with
[each release](https://pyscript.net/releases/2025.11.2/). This file contains
@@ -308,10 +321,41 @@ could create your offline-first PyScript work.
Congratulations!
-You have just created your first PyScript app. We've explored the core concepts
-needed to build yet more interesting things of your own.
-
-PyScript is extremely powerful, and these beginner steps only just scratch the
-surface. To learn about PyScript in more depth, check out
-[our user guide](user-guide/index.md) or
-[explore our example applications](../examples).
+You have just created your first PyScript app.
+
+But PyScript can do so much more than update text on a page. Here are some of
+the powerful and fun capabilities waiting for you:
+
+**Rich output with `display()`**: Instead of manually finding elements and
+setting their content, you can use PyScript's `display()` function to show
+Python objects, images, and charts directly on your page. Imagine displaying a
+matplotlib chart or a pandas DataFrame with a single function call. Learn more in
+the [user guide](user-guide/display.md).
+
+**Create dynamic interfaces**: The `pyscript.web` module lets you create entire
+user interfaces from Python code - build forms, tables, and interactive
+components without writing HTML. You can compose complex layouts using familiar
+Python syntax. Explore the possibilities in the
+[DOM interaction guide](user-guide/dom.md#pyscriptweb).
+
+**Handle any browser event**: Beyond simple clicks, you can respond to
+keyboard input, mouse movements, form submissions, and more. PyScript makes it
+easy to create rich, interactive experiences. See the
+[events guide](user-guide/events.md) for details.
+
+**Access device capabilities**: Capture photos from the camera, record audio,
+read files from the user's computer, store data locally - PyScript gives your
+Python code access to modern web capabilities. Check out the
+[media guide](user-guide/media.md) and [filesystem guide](user-guide/filesystem.md)
+for more information.
+
+**Build fast, responsive apps**: Use web workers to run Python code in the
+background, keeping your interface smooth even during heavy computation. Perfect
+for data processing, simulations, or any CPU-intensive task.
+[Learn about workers here](user-guide/workers.md).
+
+The [user guide](user-guide/index.md) explores all these topics and more. Keep
+reading to discover what you can build with PyScript! And if you build something
+wonderful, please
+[share it via our community discord server](https://discord.gg/HxvBtukrg2) (we
+love to learn about and celebrate what folks have been up to).
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/game.py b/docs/example-apps/bouncing-ball/game.py
new file mode 100644
index 0000000..d6eb05d
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/game.py
@@ -0,0 +1,34 @@
+"""
+Bouncing Ball - PyGame-CE demo for PyScript.
+
+Based on the PyGame-CE quickstart tutorial.
+"""
+import asyncio
+import sys
+import pygame
+
+pygame.init()
+
+size = width, height = 320, 240
+speed = [2, 2]
+black = 0, 0, 0
+
+screen = pygame.display.set_mode(size)
+ball = pygame.image.load("intro_ball.gif")
+ballrect = ball.get_rect()
+
+while True:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ sys.exit()
+
+ ballrect = ballrect.move(speed)
+ if ballrect.left < 0 or ballrect.right > width:
+ speed[0] = -speed[0]
+ if ballrect.top < 0 or ballrect.bottom > height:
+ speed[1] = -speed[1]
+
+ screen.fill(black)
+ screen.blit(ball, ballrect)
+ pygame.display.flip()
+ await asyncio.sleep(1/60)
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/index.html b/docs/example-apps/bouncing-ball/index.html
new file mode 100644
index 0000000..ffd3467
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
PyGame-CE Bouncing Ball
+
+
+
+
+
+
Bouncing Ball
+
A simple PyGame-CE demo running in the browser.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/info.md b/docs/example-apps/bouncing-ball/info.md
new file mode 100644
index 0000000..d303d24
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/info.md
@@ -0,0 +1,40 @@
+# Bouncing Ball
+
+A simple PyGame-CE demonstration running in the browser with PyScript.
+Based on the
+[PyGame-CE quickstart tutorial](https://pyga.me/docs/tutorials/en/intro-to-pygame.html).
+
+## What it shows
+
+- Running PyGame-CE in the browser with the `py-game` script type.
+- Using `await asyncio.sleep()` for frame timing in the browser.
+- Loading game assets through PyScript configuration.
+- Basic game loop with collision detection.
+
+## How it works
+
+The game initialises a pygame display, loads a ball image, and runs an
+infinite game loop. Each frame, it updates the ball position, checks for
+wall collisions (reversing speed on impact), renders the scene, and
+yields control to the browser with `await asyncio.sleep(1/60)`.
+
+The `await` at the top level works because PyScript provides an async
+context. This wouldn't run in standard Python without wrapping in an
+async function.
+
+## Required files
+
+You'll need to download `intro_ball.webp` from the PyGame-CE repository:
+https://raw.githubusercontent.com/pygame-community/pygame-ce/80fe4cb9f89aef96f586f68d269687572e7843f6/docs/reST/tutorials/assets/intro_ball.gif
+
+Place it in the same directory as the other files.
+
+## Running locally
+
+Serve the files with any web server:
+
+```sh
+python -m http.server 8000
+```
+
+Then visit `http://localhost:8000/` in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/intro_ball.gif b/docs/example-apps/bouncing-ball/intro_ball.gif
new file mode 100644
index 0000000000000000000000000000000000000000..bbc4a95fe883abaa4ad009669fc2d0f8c367aaf9
GIT binary patch
literal 5015
zcmeHK_g52Guud-^5b0GzkrrL)2w3P+TsKCkf(C*jMT#IT=%zseL
tPBz>Y&c@6SR`gM#WJ~6BoGOBG69Fj<9YLBQmJ?oY?+cH61lNO
zvQ3RhV9Jq3iWMTUR3MYH#hYB2B$6pN{iX7Cx!|WX>8Hs1ry%I32=`OQT$d78c(`=|
zZbg)|!V6jvWvqyqN=}em#8$9_q+*VY?JX0~q&%XG86;)8Z6=m-k_0>so6Q!p31ViX
zjExi1+&FBSi0CF@X9#FP99j}D!;Q^k(`Ynyk~b%5lM|UVB9X~v6O)((Hx4l}DI-4+H{o!D`wtb^_ah$;Ayp*%
zr?2JMzkqVDShA99SAG8uFmbAp^sh}rwYA3%r^&BJ^L>t(JO9A02OxEvtd{o5Z$Y;h
zIK@dM5)c^zf9rqmUA=W`!4NVPdm$s^xe%$pfwv~={lzp7XKbIQp1g8$`7b-^4n8P8vzA7f4Tm
zUGm2*FAz!-B4APZ&qhPd4vjBS%!Q#ARS{h-R@K+izeo_8!nEb)lV4#{Jxa>r9FG{8
z@Uq4KFsh@qO)-Oq@4P1TMr>43^7sIVdf_n`r!hhs$@fAh)x$Z4?d59IZpuV0Cw+3~
z-66v_e~3yuysi%n9RpPaeCbf~j4a%fJF*_m;Hl#iShD(Ln=0n!Nn3-$TFy|Mcim#N
z^gS|jqQCaafzN-YHXN{=<8HB8X88;tdQqVId$B68!G?=*AeCt|(XN{|Q3mm?SuO4<
z{oA#hN5)|0oD|ir6rtbs*nHrk-=|wp%cA?I(XWKY+mSY{qq?eo>K5>`4!1P-)RXlI
z%#VH#?T$umyP!v%z+c_y`Mg{vRn)$P*lL&f-p5xAz8&`IMu7}YF`7T?@BS^0f*g$1
zajwT4%j5GeeKtzi==xDwB9f@_?Pz1}JGZ(_`#2`~&l332{s!9OT|0313$fYrmDf?6
zp}gSPk5|SD=hnc5Wrv_zhm1@6#^bq1V1|h$z<%FUk9`lHVs7HI5QE2OhhsKY=iDS0
z^*YxeOP2?4Yx1w8Fj
z*g0&`B)g58^!Uy1UkfEmy7r`G_;=sbl9|-OC$?b`??cWLhE7RESnkpHAb`0
zUoDB9dWJsIPIcxVxuh1@qI$*?SLxi?N^7RLU*X)k8c_mkZ*qo7Hz-(;gA%{9UG@W~?T%2EWXGm$|aD)m9mC
zuhi1C=?9w^u!f|QdeY&0x%K1zRa68wxcPeVb88cKuefbmEiP=bN{c6SyRIg4jY&Ni
zWW%qMW`bC7;tI((mXhnvTgY6ohz%
z-lz|cyIRq3VBXs4US8-wb=B$_%V&+3)99`X!2S<@@6W+O;H}(+em~axOhF>ez<;V)
z^!UAddHbW;;t<$D&zIPHvvZ#sUDb0XRN6-_e0^yBmSzvti{eQ!T*!uY%vV72#|@f)%RtY~-(`$JS_M``W@3`jHC
zW5Muh#rSVMf|AJ{CvsIDOI_Ym%X;rF8h;x`|5^h)k&ULA(fZQdIPW(uA9S6Y9=KPw
zOZ_Q3`a3w@yJuK^1B?cwpVgd;}R4FJ3@|GI@$1}&yERoxPk}Tlf39PPMq#dmU
zXZ;u6ol^Ix8FqdGNQ|
z+|fiOhLP3#4Mn$uc(tWbZJ7OciK1R<1OHvRAF9M@>b3QTO&y5ovb!$ZGK>F;grIjD
zJuj-+gWz~9nc-KtL)!7O`>ySKpF5SfBukzg4ovYVdm_z0oYftD{jHC6>~qJym6}dr
z7}fnjfkP&{4<{0_G6G9%?LdwfB0uD59-r^@U+dU@k>Ph}5PsOCTc`EwZx1zRNk}^&io~k?0_+v|Q`?1zalc)4Y
zMosf%Yls)#uHeo;I`Aro`hixBU4v(fiaiKTPe>cEe21@gjjMZ8&E?nWH=8cb6aye$
zmY;J5ZHIAU5AF>2d~fLmRb0DfO>33phj6m&ZKzA$&?vmEH>Jl
z44VknZi5%U*qwzmL`nr{kIji9Mxm4eE
zztA4IcIZXF9Ma#X5wF{6XETF6u;2|fa6ycNR?ubZ`HzgFefAfQ0(VXAG=%3zk3kM&
zPmMgLw}z6GRtO5C@bc}=7J#vNQ5a1IHhyHpkR78PoVJhcmB21m^N_lsXF94V!PWWH
zzKg(rNs-vV>W4ZP9W+bt7qg}fn74Lp$6_aA&lb1RJCDKBNbvlHQ@=BEDF2m6!i&Ko
zi<&M5EuCpz4xH%IPXpPp%`O}j6-4_UxJN5_VjVNLsT9T|$_oz#1P*y`4|dSs96HN2
zrLoV@UN|dNY>_zbJiVgmR4;ODv1srNjlnI0Z)Iu(>;}1+eU4#el6R=FKy157
zp9#puM(qnN=f@+Odp633$7p-;nCaNk=o{K-gUe+Mi<7JS5TD3YHl#*~tRICp_La2j
zA&8$-4BRUEZi8lUCjV)gc;vE1>_K@gcCXIa#aA6K$b{cnJmFTAczPz8fvP~-SM)*-
znv*Li>7euWy66slH6g$>&AtoaQj5T8*~AR!7gI^)4&!Beu=bDb0Sr@JqsygHEr7qw
za=Ue%EcKwV;}H#h<;^u!w1^sB4tUS4;>?7~uo=K^l(T0-kr!dlBp7$LvQ{gLQoNp^
zSgyT%(|6lt;H7@kSLLN9o=nA5Wktu{V<;#G9rj0U$;GitcXR{4hl48~Z5uE!;o8?-
z>etUU06pJ8*uLl2u>+y#(ckJT*^E2v0&IN}vNH9hDb&;aabTS|P|wAzlwV{QuM6F<
z2(CfaG*81P$NjM#5Jrd5Iiz9F)}|{4x@|N2svkB)>N{1VanBQ?0y;`2vdSA0f?vO?
zdiN1Dl~5@z;>M7He*rmK`fQ7%5FZPi^l|9-SIKs@&4!VTl&N2#W_7ScZfk_zUw-
z<8bp8vplWKrj~jml~pxh6ErgCYSS;X^a+1N9t?5H&@JAuVIUnC1Js$CX*>S_pe@aY*=(JD;=mv;qe)9xD7NN(Cb!Rv_c1_)|@{EM$!=l-S8Um1ZV+R1fSE_DEYBeeK|FR`)LPShPc
z7!;Flhxk3MF$J#GGeLVwHN@HqlfN1|uY#r%TLxd)y0W0(kNTc)uRhPdvD0UQPw2LG`+_kmWd*jch88*(|FwN6?W2zr9
z>&;p?U_B=|2ZiUe;CGggzZ}<=-J$iE>q7&D;yisT?Cy*!9;<#n7$qlmm7g#8oU
z7-ASjMGxu;%*^FmBr>~jMQ0uzh{az^gTk{U38W9A<79B42eaxKAft$@>S@xRXTKAG
zq$c?auEJ7AVZ?>wd7-^+Kw?i_Z}?V0yvNl9j4J{IZ1RMC|J1G48I{d~ztM>}IH#L4
zO0#z6njfiTerZ!**tTWDuI`)rA4mIaFpcNA$o|eMQ}4(_i~+}uBdie*-KnjX{Aup^
zpM%1Y!4qF$^=T0tT{8$7OehR10}QGFET(gLvo{(bP~@jNmjcYt&D*eSv`RYm#KV#y
zu>I-mHN<{*t(KyOQKH8mM#$I&sKxF6Z+|0G@BxcEB*^9=f`5ASFz|IBAZ+Wk`C!-h
z4|__@y*-16Jpj#3IpMDW{Jvw0O>#NPd>l#PwdW!P4I~|U$
z?9Biv9+-^Q7ppV>dX^;eeF-~4LOM!s`FuzD_JDC%XtFmZdkUb#s#dG(-(fr?r~b-G
zL!RY&g(e4Nutv3-hr`k2IeX)_FFRfkq#Ifu91M(>%!Kf%NPnw4x@0hp1dhZ)gGrF>
zEYNm45b^Gv{eKH%(a5uVA;;3(3zEms_Q=qMLx=E?02a)}4rI=PZoTGmc>%^|!IDq`
zl(>M#YdR0&zyTykB@0%ShK&0#(Y9ru`v(pF3y3J+Bz*xg&P0OBk+mN*XvXk6dnc!&
LCL@ed0FVCxQs}HF
literal 0
HcmV?d00001
diff --git a/docs/example-apps/bouncing-ball/pyscript.toml b/docs/example-apps/bouncing-ball/pyscript.toml
new file mode 100644
index 0000000..d226b63
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/pyscript.toml
@@ -0,0 +1,2 @@
+[files]
+"intro_ball.gif" = ""
\ No newline at end of file
diff --git a/docs/example-apps/colour-picker/index.html b/docs/example-apps/colour-picker/index.html
new file mode 100644
index 0000000..ec5b606
--- /dev/null
+++ b/docs/example-apps/colour-picker/index.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+ Interactive Colour Picker
+
+
+
+
+
+ Interactive Colour Picker
+
+
+
#3498DB
+
+
+
+
+ Red
+ Blue
+ Green
+ Orange
+ Purple
+ Teal
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/colour-picker/info.md b/docs/example-apps/colour-picker/info.md
new file mode 100644
index 0000000..6d1b175
--- /dev/null
+++ b/docs/example-apps/colour-picker/info.md
@@ -0,0 +1,94 @@
+# Interactive Colour Picker
+
+A colour picker application demonstrating various event handling
+patterns in PyScript.
+
+## What it demonstrates
+
+**Multiple event types:**
+- `input` events - RGB sliders update in real-time.
+- `change` events - Number inputs and hex input.
+- `click` events - Preset buttons and history colours.
+
+**Stacked decorators:**
+- Single function handling multiple sliders with `@when` stacked three
+ times.
+
+**Custom events:**
+- `colour_changed` Event for decoupling colour updates from history
+ management.
+- Shows how to separate concerns in your application.
+
+**Working with form inputs:**
+- Range sliders, number inputs, text inputs.
+- Synchronising values across different input types.
+- Validating and clamping values.
+
+**Dynamic UI updates:**
+- Updating display colour.
+- Maintaining colour history.
+- Creating history elements dynamically.
+
+## Features
+
+- Adjust colours using RGB sliders.
+- Enter RGB values directly with number inputs.
+- Enter hex colour codes.
+- Quick selection from preset colours.
+- Colour history (last 10 colours).
+- Click history to restore colours.
+- Real-time colour display with hex code.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Event handling logic demonstrating various patterns.
+
+## Key patterns demonstrated
+
+### Stacking decorators
+
+```python
+@when("input", "#red-slider")
+@when("input", "#green-slider")
+@when("input", "#blue-slider")
+def handle_slider_change(event):
+ # Single function handles all three sliders.
+ pass
+```
+
+### Custom events for decoupling
+
+```python
+# Define custom event.
+colour_changed = Event()
+
+# Trigger it when colour updates.
+colour_changed.trigger(hex_colour)
+
+# Handle it separately.
+@when(colour_changed)
+def handle_colour_changed(hex_colour):
+ add_to_history(hex_colour)
+```
+
+### Working with form inputs
+
+```python
+@when("input", "#red-slider")
+def handle_slider_change(event):
+ # Get value from slider.
+ value = int(event.target.value)
+ # Update display.
+ update_display(value)
+```
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/colour-picker/main.py b/docs/example-apps/colour-picker/main.py
new file mode 100644
index 0000000..725e77b
--- /dev/null
+++ b/docs/example-apps/colour-picker/main.py
@@ -0,0 +1,195 @@
+"""
+Interactive Colour Picker - demonstrating event handling in PyScript.
+
+Shows:
+- Multiple event types (input, click, change)
+- Custom events for decoupled logic
+- Working with form inputs
+- Dynamic UI updates
+"""
+from pyscript import when, Event
+from pyscript.web import page
+
+
+# Custom event for when colour changes.
+colour_changed = Event()
+
+# Colour history (limited to 10).
+history = []
+
+
+def rgb_to_hex(r, g, b):
+ """
+ Convert RGB values to hex colour string.
+ """
+ return f"#{r:02X}{g:02X}{b:02X}"
+
+
+def hex_to_rgb(hex_colour):
+ """
+ Convert hex colour string to RGB tuple.
+ """
+ hex_colour = hex_colour.lstrip("#")
+ return tuple(int(hex_colour[i:i+2], 16) for i in (0, 2, 4))
+
+
+def get_current_rgb():
+ """
+ Get current RGB values from sliders.
+ """
+ r = int(page["#red-slider"].value)
+ g = int(page["#green-slider"].value)
+ b = int(page["#blue-slider"].value)
+ return r, g, b
+
+
+def update_display(hex_colour):
+ """
+ Update the colour display with the given hex colour.
+ """
+ display = page["#colour-display"]
+ display.style.backgroundColor = hex_colour
+ display.content = hex_colour
+
+
+def update_controls(r, g, b):
+ """
+ Update all controls to match RGB values.
+ """
+ # Update sliders.
+ page["#red-slider"].value = r
+ page["#green-slider"].value = g
+ page["#blue-slider"].value = b
+
+ # Update number inputs.
+ page["#red-value"].value = r
+ page["#green-value"].value = g
+ page["#blue-value"].value = b
+
+ # Update hex input.
+ hex_colour = rgb_to_hex(r, g, b)
+ page["#hex-input"].value = hex_colour
+
+ # Update display.
+ update_display(hex_colour)
+
+ # Trigger custom event.
+ colour_changed.trigger(hex_colour)
+
+
+def add_to_history(hex_colour):
+ """
+ Add colour to history, maintaining max of 10 items.
+ """
+ if hex_colour in history:
+ return
+
+ history.insert(0, hex_colour)
+ if len(history) > 10:
+ history.pop()
+
+ # Update history display.
+ render_history()
+
+
+def render_history():
+ """
+ Render colour history.
+ """
+ from pyscript.web import div
+
+ container = page["#history-colours"]
+ container.clear()
+
+ for colour in history:
+ colour_div = div(Class="history-colour", title=colour)
+ colour_div.style.backgroundColor = colour
+ colour_div.dataset.colour = colour
+ container.append(colour_div)
+
+
+@when("input", "#red-slider")
+@when("input", "#green-slider")
+@when("input", "#blue-slider")
+def handle_slider_change(event):
+ """
+ Handle RGB slider changes.
+ """
+ r, g, b = get_current_rgb()
+ update_controls(r, g, b)
+
+
+@when("change", "#red-value")
+@when("change", "#green-value")
+@when("change", "#blue-value")
+def handle_number_change(event):
+ """
+ Handle number input changes.
+ """
+ r = int(page["#red-value"].value)
+ g = int(page["#green-value"].value)
+ b = int(page["#blue-value"].value)
+
+ # Clamp values.
+ r = max(0, min(255, r))
+ g = max(0, min(255, g))
+ b = max(0, min(255, b))
+
+ update_controls(r, g, b)
+
+
+@when("change", "#hex-input")
+def handle_hex_change(event):
+ """
+ Handle hex input changes.
+ """
+ hex_colour = event.target.value.strip()
+
+ # Validate hex colour.
+ if not hex_colour.startswith("#"):
+ hex_colour = "#" + hex_colour
+
+ try:
+ r, g, b = hex_to_rgb(hex_colour)
+ update_controls(r, g, b)
+ except (ValueError, IndexError):
+ # Invalid hex colour, ignore.
+ pass
+
+
+@when("click", ".preset-btn")
+def handle_preset_click(event):
+ """
+ Handle preset colour button clicks.
+ """
+ hex_colour = event.target.dataset.colour
+ r, g, b = hex_to_rgb(hex_colour)
+ update_controls(r, g, b)
+
+
+@when("click", ".history-colour")
+def handle_history_click(event):
+ """
+ Handle clicks on history colours.
+ """
+ hex_colour = event.target.dataset.colour
+ r, g, b = hex_to_rgb(hex_colour)
+ update_controls(r, g, b)
+
+
+@when(colour_changed)
+def handle_colour_changed(hex_colour):
+ """
+ Handle custom colour changed event.
+
+ This demonstrates decoupling - the history management doesn't need
+ to know about sliders, presets, or hex inputs.
+ """
+ add_to_history(hex_colour)
+
+
+# Initial setup.
+r, g, b = get_current_rgb()
+hex_colour = rgb_to_hex(r, g, b)
+update_display(hex_colour)
+add_to_history(hex_colour)
\ No newline at end of file
diff --git a/docs/example-apps/display-demo/index.html b/docs/example-apps/display-demo/index.html
new file mode 100644
index 0000000..d252759
--- /dev/null
+++ b/docs/example-apps/display-demo/index.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+ Display Demo
+
+
+
+
+
+ Display Capabilities Demo
+
+
+
+
Basic Types
+
+
Show Basic Types
+
+
+
+
HTML Content
+
+
Show HTML
+
+
+
+
Custom Objects
+
+
Show Custom Object
+
+
+
+
Multiple Values
+
+
Show Multiple
+
+
+
+
Data Cards
+
+
Generate Cards
+
+
+
+
Incremental Updates
+
+
Start Updates
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/display-demo/info.md b/docs/example-apps/display-demo/info.md
new file mode 100644
index 0000000..5b95342
--- /dev/null
+++ b/docs/example-apps/display-demo/info.md
@@ -0,0 +1,98 @@
+# Display Demo
+
+A comprehensive demonstration of PyScript's `display()` function and its
+various capabilities.
+
+## What it demonstrates
+
+**Basic types:**
+- Strings, numbers, booleans, lists, dictionaries.
+- Automatic HTML escaping for safety.
+
+**HTML content:**
+- Using `HTML()` to render unescaped HTML.
+- Creating styled content boxes.
+- Building rich interfaces.
+
+**Custom objects:**
+- Implementing `_repr_html_()` for custom rendering.
+- Creating reusable display components.
+- Table generation with custom styling.
+
+**Multiple values:**
+- Displaying several values in one call.
+- Appending vs. replacing content.
+
+**Incremental updates:**
+- Building UIs progressively.
+- Showing status updates with delays.
+- Creating loading sequences.
+
+## Features
+
+Six interactive panels demonstrating:
+
+1. **Basic Types** - Standard Python objects.
+2. **HTML Content** - Rich formatted content.
+3. **Custom Objects** - Classes with custom display logic.
+4. **Multiple Values** - Batch display operations.
+5. **Data Cards** - Styled metric cards using custom classes.
+6. **Incremental Updates** - Progressive UI building with async.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Display demonstrations with custom classes.
+
+## Key patterns demonstrated
+
+### Custom display representations
+
+```python
+class MetricCard:
+ def __init__(self, label, value, colour):
+ self.label = label
+ self.value = value
+ self.colour = colour
+
+ def _repr_html_(self):
+ return f"{self.label}: {self.value}
"
+
+# Displays with custom HTML.
+display(MetricCard("Users", "1,234", "#667eea"))
+```
+
+### Multiple format support
+
+```python
+class DataTable:
+ def _repr_html_(self):
+ return ""
+
+ def __repr__(self):
+ return "Plain text table"
+
+# Automatically uses HTML when available.
+display(table)
+```
+
+### Incremental building
+
+```python
+async def build_ui():
+ display("Step 1", target="output", append=False)
+ await asyncio.sleep(1)
+ display("Step 2", target="output")
+ await asyncio.sleep(1)
+ display("Complete!", target="output")
+```
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/display-demo/main.py b/docs/example-apps/display-demo/main.py
new file mode 100644
index 0000000..c8dd971
--- /dev/null
+++ b/docs/example-apps/display-demo/main.py
@@ -0,0 +1,205 @@
+"""
+Display Demo - showcasing display() capabilities in PyScript.
+
+Demonstrates:
+- Displaying basic Python types
+- HTML content with the HTML() wrapper
+- Custom objects with _repr_html_()
+- Multiple values at once
+- Building UIs incrementally
+- Replacing vs. appending content
+"""
+from pyscript import display, HTML, when
+import asyncio
+
+
+class MetricCard:
+ """
+ A metric card that displays with custom HTML.
+ """
+ def __init__(self, label, value, colour="#667eea"):
+ self.label = label
+ self.value = value
+ self.colour = colour
+
+ def _repr_html_(self):
+ """
+ Custom HTML representation.
+ """
+ return f"""
+
+
{self.label}
+
{self.value}
+
+ """
+
+ def _darken_colour(self, colour):
+ """
+ Simple colour darkening for gradient.
+ """
+ # Very simple darkening - just for demo purposes.
+ if colour == "#667eea":
+ return "#764ba2"
+ elif colour == "#f093fb":
+ return "#f5576c"
+ elif colour == "#4facfe":
+ return "#00f2fe"
+ return "#333333"
+
+
+class DataTable:
+ """
+ A table with multiple representation formats.
+ """
+ def __init__(self, headers, rows):
+ self.headers = headers
+ self.rows = rows
+
+ def _repr_html_(self):
+ """
+ HTML table representation.
+ """
+ html = ""
+
+ # Headers.
+ html += ""
+ for header in self.headers:
+ html += f""
+ html += f"{header} "
+ html += " "
+
+ # Rows.
+ for i, row in enumerate(self.rows):
+ bg = "#f8f9fa" if i % 2 == 0 else "white"
+ html += f""
+ for cell in row:
+ html += f"{cell} "
+ html += " "
+
+ html += "
"
+ return html
+
+ def __repr__(self):
+ """
+ Plain text representation.
+ """
+ lines = ["\t".join(self.headers)]
+ lines.extend(["\t".join(str(c) for c in row) for row in self.rows])
+ return "\n".join(lines)
+
+
+@when("click", "#btn-basic")
+def show_basic_types(event):
+ """
+ Display basic Python types.
+ """
+ display("=== Basic Types ===", target="basic-output", append=True)
+ display("String: Hello, World!", target="basic-output", append=True)
+ display(f"Number: {42}", target="basic-output", append=True)
+ display(f"Float: {3.14159}", target="basic-output", append=True)
+ display(f"Boolean: {True}", target="basic-output", append=True)
+ display(f"List: {[1, 2, 3, 4, 5]}", target="basic-output", append=True)
+ display(f"Dict: {{'name': 'Alice', 'age': 30}}", target="basic-output", append=True)
+
+
+@when("click", "#btn-html")
+def show_html_content(event):
+ """
+ Display HTML content.
+ """
+ # Clear and show HTML.
+ display(HTML("Rich HTML Content "),
+ target="html-output", append=False)
+
+ display(HTML("""
+ This is bold and this is italic .
+ """), target="html-output")
+
+ display(HTML("""
+
+ """), target="html-output")
+
+ display(HTML("""
+
+ Item one
+ Item two
+ Item three
+
+ """), target="html-output")
+
+
+@when("click", "#btn-custom")
+def show_custom_object(event):
+ """
+ Display custom objects with _repr_html_.
+ """
+ display("=== Custom Object ===",
+ target="custom-output", append=False)
+
+ # Display a table.
+ table = DataTable(
+ ["Name", "Score", "Grade"],
+ [
+ ["Alice", 95, "A"],
+ ["Bob", 87, "B"],
+ ["Carol", 92, "A"],
+ ["Dave", 78, "C"]
+ ]
+ )
+ display(table, target="custom-output")
+
+
+@when("click", "#btn-multi")
+def show_multiple_values(event):
+ """
+ Display multiple values at once.
+ """
+ display("First value", "Second value", "Third value",
+ target="multi-output", append=True)
+
+
+@when("click", "#btn-cards")
+def show_metric_cards(event):
+ """
+ Display metric cards.
+ """
+ display(MetricCard("Total Users", "1,234", "#667eea"),
+ target="cards-output", append=False)
+ display(MetricCard("Revenue", "$56,789", "#f093fb"),
+ target="cards-output")
+ display(MetricCard("Growth", "+23%", "#4facfe"),
+ target="cards-output")
+
+
+@when("click", "#btn-incremental")
+async def show_incremental_updates(event):
+ """
+ Build UI incrementally with delays.
+ """
+ display(HTML("Loading data... "),
+ target="incremental-output", append=False)
+ await asyncio.sleep(0.5)
+
+ display(HTML("✓ Connected to server
"),
+ target="incremental-output")
+ await asyncio.sleep(0.5)
+
+ display(HTML("✓ Fetching records
"),
+ target="incremental-output")
+ await asyncio.sleep(0.5)
+
+ display(HTML("✓ Processing data
"),
+ target="incremental-output")
+ await asyncio.sleep(0.5)
+
+ display(HTML("Complete! ✓ "),
+ target="incremental-output")
+
+
+# Initial message.
+display(HTML("Click buttons to see different "
+ "display examples.
"),
+ target="basic-output")
\ No newline at end of file
diff --git a/docs/example-apps/note-taker/index.html b/docs/example-apps/note-taker/index.html
new file mode 100644
index 0000000..460bf45
--- /dev/null
+++ b/docs/example-apps/note-taker/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Note Taker
+
+
+
+
+
+ Note Taker
+ Save notes to your local filesystem.
+
+
+ Select Folder
+ Save Note
+
+
+
+
+ Click "Select Folder" to begin...
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/note-taker/info.md b/docs/example-apps/note-taker/info.md
new file mode 100644
index 0000000..7a15ea8
--- /dev/null
+++ b/docs/example-apps/note-taker/info.md
@@ -0,0 +1,36 @@
+# Note Taker
+
+A simple note-taking application demonstrating local filesystem access.
+
+## What it shows
+
+- Mounting a local directory on user interaction.
+- Writing files to the mounted directory.
+- Syncing changes to persist them locally.
+- Proper error handling.
+
+## How it works
+
+Click "Select Folder" to mount a local directory. The browser will
+prompt you to choose a folder. Once mounted, you can type notes and
+save them to your chosen folder.
+
+The key pattern:
+
+```python
+# Mount (user selects folder).
+await fs.mount("/notes")
+
+# Write files.
+with open("/notes/my-note.txt", "w") as f:
+ f.write(note_text)
+
+# Sync to persist changes.
+await fs.sync("/notes")
+```
+
+## Browser support
+
+This only works in Chromium-based browsers (Chrome, Edge, Brave,
+Vivaldi). Firefox and Safari don't support the File System Access API
+yet.
\ No newline at end of file
diff --git a/docs/example-apps/note-taker/main.py b/docs/example-apps/note-taker/main.py
new file mode 100644
index 0000000..8567c06
--- /dev/null
+++ b/docs/example-apps/note-taker/main.py
@@ -0,0 +1,45 @@
+"""
+Note Taker - demonstrating local filesystem access.
+"""
+from pyscript import when, fs
+from pyscript.web import page
+
+
+@when("click", "#mount-btn")
+async def mount_folder(event):
+ """
+ Mount a local folder for saving notes.
+ """
+ status = page["#status"]
+ status.content = "Please select a folder..."
+
+ try:
+ await fs.mount("/notes")
+
+ # Enable the UI.
+ page["#save-btn"].disabled = False
+ page["#note"].disabled = False
+ status.content = "Ready! Type your note and click Save."
+ except Exception as e:
+ status.content = f"Error: {e}"
+
+
+@when("click", "#save-btn")
+async def save_note(event):
+ """
+ Save the note to the mounted folder.
+ """
+ status = page["#status"]
+ note_text = page["#note"].value
+
+ try:
+ # Write the note.
+ with open("/notes/my-note.txt", "w") as f:
+ f.write(note_text)
+
+ # Sync to local filesystem.
+ await fs.sync("/notes")
+
+ status.content = "Note saved successfully!"
+ except Exception as e:
+ status.content = f"Error saving: {e}"
\ No newline at end of file
diff --git a/docs/example-apps/overview.md b/docs/example-apps/overview.md
new file mode 100644
index 0000000..a9f2c99
--- /dev/null
+++ b/docs/example-apps/overview.md
@@ -0,0 +1,3 @@
+# Example Apps
+
+TODO: FINISH THIS!
\ No newline at end of file
diff --git a/docs/example-apps/photobooth/index.html b/docs/example-apps/photobooth/index.html
new file mode 100644
index 0000000..76efdf7
--- /dev/null
+++ b/docs/example-apps/photobooth/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ Photobooth
+
+
+
+
+
+ Photobooth
+ Capture still frames from your webcam.
+
+
+ Start Camera
+ Capture Photo
+
+
+
+
+
Live View
+
+
+
+
Captured Photo
+
+
+
+
+ Click "Start Camera" to begin.
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/photobooth/info.md b/docs/example-apps/photobooth/info.md
new file mode 100644
index 0000000..b274d16
--- /dev/null
+++ b/docs/example-apps/photobooth/info.md
@@ -0,0 +1,33 @@
+# Photobooth
+
+A simple webcam application demonstrating media device access and still
+frame capture.
+
+## What it shows
+
+- Requesting camera access with `Device.request_stream()`.
+- Displaying live video in a video element.
+- Capturing still frames from video using canvas.
+- Proper error handling for permission denial.
+
+## How it works
+
+Click "Start Camera" to request webcam access. Once granted, the live
+video feed appears. Click "Capture Photo" to grab the current frame and
+display it as a still image on the canvas.
+
+The key technique is using the canvas `drawImage()` method to copy the
+current video frame:
+
+```python
+# Get the canvas context.
+ctx = canvas.getContext("2d")
+
+# Draw the current video frame.
+ctx.drawImage(video, 0, 0, width, height)
+```
+
+## Browser support
+
+This requires a browser with webcam support and HTTPS (or localhost).
+The user must grant camera permission when prompted.
\ No newline at end of file
diff --git a/docs/example-apps/photobooth/main.py b/docs/example-apps/photobooth/main.py
new file mode 100644
index 0000000..0ebf10b
--- /dev/null
+++ b/docs/example-apps/photobooth/main.py
@@ -0,0 +1,55 @@
+"""
+Photobooth - demonstrating webcam capture and still frame extraction.
+"""
+from pyscript import when
+from pyscript.media import Device
+from pyscript.web import page
+
+
+# Track the current stream.
+current_stream = None
+
+
+@when("click", "#start-btn")
+async def start_camera(event):
+ """
+ Start the camera and display live video.
+ """
+ global current_stream
+
+ status = page["#status"]
+ status.content = "Requesting camera access..."
+
+ try:
+ # Request video stream.
+ current_stream = await Device.request_stream(video=True)
+
+ # Display in video element.
+ video = page["#camera"]
+ video.srcObject = current_stream
+
+ # Update UI.
+ page["#start-btn"].disabled = True
+ page["#capture-btn"].disabled = False
+ status.content = "Camera ready! Click 'Capture Photo' to take a picture."
+ except Exception as e:
+ status.content = f"Error accessing camera: {e}"
+
+
+@when("click", "#capture-btn")
+def capture_photo(event):
+ """
+ Capture a still frame from the video stream.
+ """
+ video = page["#camera"]
+ canvas = page["#photo"]
+
+ # Get the canvas 2D context.
+ ctx = canvas.getContext("2d")
+
+ # Draw the current video frame onto the canvas.
+ ctx.drawImage(video, 0, 0, 400, 300)
+
+ # Update status.
+ status = page["#status"]
+ status.content = "Photo captured!"
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/index.html b/docs/example-apps/pirate-translator/index.html
new file mode 100644
index 0000000..302ecaa
--- /dev/null
+++ b/docs/example-apps/pirate-translator/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ 🦜 Polyglot - Piratical PyScript
+
+
+
+
+ Polyglot 🦜 💬 🇬🇧 ➡️ 🏴☠️
+ Translate English into Pirate speak...
+
+ Translate
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/info.md b/docs/example-apps/pirate-translator/info.md
new file mode 100644
index 0000000..911a6e0
--- /dev/null
+++ b/docs/example-apps/pirate-translator/info.md
@@ -0,0 +1,35 @@
+# Pirate Translator 🦜 💬 🇬🇧 ➡️ 🏴☠️
+
+A simple PyScript application that translates English text into Pirate speak.
+
+## What it demonstrates
+
+- Basic PyScript application structure (HTML, Python, configuration).
+- Using `pyscript.web` to interact with page elements.
+- Event handling with the `@when` decorator.
+- Installing and using third-party Python packages ([arrr](https://arrr.readthedocs.io/en/latest/)).
+
+## Files
+
+- `index.html` - The web page to display.
+- `main.py` - Python code that handles the translation.
+- `pyscript.json` - Configuration specifying required packages.
+
+## How it works
+
+1. User types English text into an input field.
+2. User clicks the "Translate" button.
+3. The `@when` decorator connects the button's click event to `translate_english`.
+4. Function retrieves the input text using `web.page["english"]`.
+5. Text is translated using the `arrr` library.
+6. Result is displayed in the output div using `web.page["output"]`.
+
+## Running locally
+
+Simply serve these files from a web server. For example:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/main.py b/docs/example-apps/pirate-translator/main.py
new file mode 100644
index 0000000..a814718
--- /dev/null
+++ b/docs/example-apps/pirate-translator/main.py
@@ -0,0 +1,16 @@
+"""
+Pirate translator - translate English to Pirate speak.
+"""
+import arrr
+from pyscript import web, when
+
+
+@when("click", "#translate-button")
+def translate_english(event):
+ """
+ Translate English text to Pirate speak.
+ """
+ input_text = web.page["english"]
+ english = input_text.value
+ output_div = web.page["output"]
+ output_div.innerText = arrr.translate(english)
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/pyscript.json b/docs/example-apps/pirate-translator/pyscript.json
new file mode 100644
index 0000000..89e5ce3
--- /dev/null
+++ b/docs/example-apps/pirate-translator/pyscript.json
@@ -0,0 +1,3 @@
+{
+ "packages": ["arrr"]
+}
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/index.html b/docs/example-apps/prime-worker/index.html
new file mode 100644
index 0000000..259a3ce
--- /dev/null
+++ b/docs/example-apps/prime-worker/index.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+ Prime Numbers with Workers
+
+
+
+
+
+ Prime Number Calculator
+ Main thread stays responsive
+
+
+ Find primes up to:
+
+ Find Primes
+ Stop
+
+
+
+
+
+ Use NumPy (faster)
+
+
+
+ Enter a number and click Find Primes...
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/info.md b/docs/example-apps/prime-worker/info.md
new file mode 100644
index 0000000..0fe8a17
--- /dev/null
+++ b/docs/example-apps/prime-worker/info.md
@@ -0,0 +1,140 @@
+# Prime Number Calculator with Workers
+
+A demonstration of PyScript workers showing how to keep the main thread
+responsive whilst performing heavy computation in a background worker.
+
+## What it demonstrates
+
+**Worker architecture:**
+- **Main thread**: MicroPython (lightweight, fast startup, responsive UI).
+- **Worker thread**: Pyodide with numpy (heavy computation, numerical
+ libraries).
+- Clear separation of concerns.
+
+**Key patterns:**
+- Starting a worker from the main thread.
+- Calling worker methods with `await`.
+- Sending incremental results back via callbacks.
+- Using `pyscript.sync` to expose functions between threads.
+- Keeping the main thread responsive during computation.
+
+**Visual feedback:**
+- Animated "heartbeat" proves main thread never blocks.
+- Real-time display of primes as they're found.
+- Status updates showing worker progress.
+
+## How it works
+
+### Main thread (MicroPython)
+
+The main thread handles the user interface:
+
+1. Gets reference to the worker via `pyscript.workers`.
+2. Registers a callback function (`handle_prime`) via `pyscript.sync`.
+3. Calls the worker's `find_primes()` method when the button is clicked.
+4. Receives prime numbers via the callback and updates the display.
+5. Stays responsive throughout (watch the pulsing green dot).
+
+### Worker thread (Pyodide)
+
+The worker does the heavy lifting:
+
+1. Exposes `find_primes()` method via `@sync` decorator.
+2. Uses numpy's efficient array operations for the Sieve of Eratosthenes.
+3. Calls back to the main thread's `handle_prime()` for each prime found.
+4. Sends results in batches with small delays to keep UI smooth.
+5. Returns a summary when complete.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Main thread logic (MicroPython).
+- `worker.py` - Worker thread logic (Pyodide with numpy).
+- `worker-config.json` - Worker configuration (numpy package).
+
+## Key code patterns
+
+### Starting the worker
+
+```python
+# Main thread gets reference to worker defined in HTML.
+from pyscript import workers
+
+worker = await workers.py # Name from script tag's type.
+```
+
+### Calling worker methods
+
+```python
+# Main thread calls worker method (must be decorated with @sync).
+result = await worker.find_primes(10000)
+```
+
+### Worker exposing methods
+
+```python
+# Worker exposes method to main thread.
+from pyscript import sync
+
+@sync
+async def find_primes(limit):
+ # Do computation.
+ return result
+```
+
+### Callbacks from worker to main
+
+```python
+# Main thread registers callback.
+from pyscript import sync
+
+async def handle_prime(prime):
+ print(f"Got prime: {prime}")
+
+sync.handle_prime = handle_prime
+
+# Worker calls back to main thread.
+handle_prime = await sync.handle_prime
+await handle_prime(42)
+```
+
+## Why this architecture?
+
+**MicroPython on main thread:**
+- Fast startup (no heavy packages to load).
+- Lightweight (perfect for UI interactions).
+- Stays responsive (no blocking operations).
+
+**Pyodide in worker:**
+- Full Python with scientific libraries (numpy).
+- Heavy computation off the main thread.
+- Can use the full Python ecosystem.
+
+**Best of both worlds:**
+- Fast, responsive UI.
+- Powerful computation when needed.
+- Users never see a frozen interface.
+
+## Try it
+
+1. Enter a number (10 to 100,000).
+2. Click "Find Primes".
+3. Watch the green heartbeat - it never stops pulsing.
+4. See primes appear in real-time.
+
+Try interacting with the page whilst it's computing - everything stays
+smooth because the main thread is never blocked.
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
+
+**Note**: You'll need to serve with appropriate CORS headers for workers
+to access `window` and `document`. See the
+[workers guide](../../user-guide/workers.md#http-headers) for details.
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/main.py b/docs/example-apps/prime-worker/main.py
new file mode 100644
index 0000000..7dc78bd
--- /dev/null
+++ b/docs/example-apps/prime-worker/main.py
@@ -0,0 +1,89 @@
+"""
+Main thread: MicroPython handling the UI.
+"""
+from pyscript import when, workers
+from pyscript.web import page
+
+
+# Track whether computation is running.
+computing = False
+
+
+@when("click", "#find-btn")
+async def find_primes(event):
+ """
+ Ask the worker to find primes.
+ """
+ global computing
+
+ find_btn = page["#find-btn"]
+ stop_btn = page["#stop-btn"]
+ limit_input = page["#limit"]
+ output = page["#output"]
+
+ # Get and validate the limit.
+ try:
+ limit = int(limit_input.value)
+ if limit < 10 or limit > 1000000:
+ output.content = "Please enter a number between 10 and 1,000,000"
+ return
+ except ValueError:
+ output.content = "Please enter a valid number"
+ return
+
+ # Check if numpy should be used.
+ use_numpy = page["#use-numpy"].checked
+
+ # Update UI state.
+ computing = True
+ find_btn.disabled = True
+ stop_btn.disabled = False
+ limit_input.disabled = True
+ output.content = f"Computing primes up to {limit:,}..."
+
+ try:
+ # Get the worker and call its exported function.
+ worker = await workers["primes"]
+
+ # Time the computation.
+ import time
+ start = time.time()
+ result = await worker.find_primes(limit, use_numpy)
+ elapsed = time.time() - start
+
+ if computing:
+ # Convert to string properly.
+ first_20 = result['first_20']
+ primes_str = ", ".join(str(p) for p in first_20)
+
+ method = "NumPy" if use_numpy else "Pure Python"
+ output.content = f"Found {result['count']:,} primes up to {limit:,}!\n\nMethod: {method}\nTime: {elapsed:.3f} seconds\n\nFirst 20: {primes_str}"
+ except Exception as e:
+ output.content = f"Error: {e}"
+ finally:
+ # Reset UI state.
+ computing = False
+ find_btn.disabled = False
+ stop_btn.disabled = True
+ limit_input.disabled = False
+
+
+@when("click", "#stop-btn")
+def stop_computation(event):
+ """
+ Stop the computation (sets flag, doesn't actually interrupt worker).
+ """
+ global computing
+
+ computing = False
+
+ output = page["#output"]
+ output.content = "Stopped (worker completed, but result discarded)"
+
+ find_btn = page["#find-btn"]
+ stop_btn = page["#stop-btn"]
+ limit_input = page["#limit"]
+
+ find_btn.disabled = False
+ stop_btn.disabled = True
+ limit_input.disabled = False
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/pyscript.json b/docs/example-apps/prime-worker/pyscript.json
new file mode 100644
index 0000000..53070e3
--- /dev/null
+++ b/docs/example-apps/prime-worker/pyscript.json
@@ -0,0 +1,3 @@
+{
+ "packages": ["numpy"]
+}
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/worker.py b/docs/example-apps/prime-worker/worker.py
new file mode 100644
index 0000000..f1e5f55
--- /dev/null
+++ b/docs/example-apps/prime-worker/worker.py
@@ -0,0 +1,55 @@
+"""
+Worker thread: Pyodide with numpy doing the computation.
+"""
+import numpy as np
+
+
+def sieve_numpy(limit):
+ """
+ Sieve of Eratosthenes using numpy arrays.
+ """
+ is_prime = np.ones(limit + 1, dtype=bool)
+ is_prime[0] = is_prime[1] = False
+
+ for i in range(2, int(limit**0.5) + 1):
+ if is_prime[i]:
+ is_prime[i*i::i] = False
+
+ primes = np.where(is_prime)[0]
+ return [int(p) for p in primes]
+
+
+def sieve_python(limit):
+ """
+ Sieve of Eratosthenes using pure Python.
+ """
+ is_prime = [True] * (limit + 1)
+ is_prime[0] = is_prime[1] = False
+
+ for i in range(2, int(limit**0.5) + 1):
+ if is_prime[i]:
+ for j in range(i*i, limit + 1, i):
+ is_prime[j] = False
+
+ return [i for i in range(limit + 1) if is_prime[i]]
+
+
+def find_primes(limit, use_numpy=True):
+ """
+ Find all primes up to limit using Sieve of Eratosthenes.
+ """
+ if use_numpy:
+ primes_list = sieve_numpy(limit)
+ else:
+ primes_list = sieve_python(limit)
+
+ first_20 = [int(primes_list[i]) for i in range(min(20, len(primes_list)))]
+
+ return {
+ "count": len(primes_list),
+ "first_20": first_20
+ }
+
+
+# Export functions to make them accessible from main thread.
+__export__ = ["find_primes"]
\ No newline at end of file
diff --git a/docs/example-apps/task-board-ffi/index.html b/docs/example-apps/task-board-ffi/index.html
new file mode 100644
index 0000000..192ec24
--- /dev/null
+++ b/docs/example-apps/task-board-ffi/index.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+ Task Board - FFI
+
+
+
+
+
+ PyScript Task Board
+ Built with the FFI
+
+
+
+
+ High
+ Medium
+ Low
+
+
Add Task
+
+
+
+ All
+ Active
+ Completed
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/task-board-ffi/info.md b/docs/example-apps/task-board-ffi/info.md
new file mode 100644
index 0000000..e95e24a
--- /dev/null
+++ b/docs/example-apps/task-board-ffi/info.md
@@ -0,0 +1,84 @@
+# Task Board - FFI Version
+
+The same task management application as the pyscript.web version, but
+implemented using the FFI (foreign function interface) with direct
+JavaScript API calls.
+
+## What it demonstrates
+
+- **Finding elements**: Using `document.getElementById()` and
+ `document.querySelectorAll()`.
+- **Creating elements**: Using `document.createElement()`.
+- **Modifying attributes**: Setting properties like `textContent`,
+ `className`, `checked`.
+- **Working with classes**: Using `classList.add()`,
+ `classList.remove()`.
+- **Collections**: Iterating over NodeLists from `querySelectorAll()`.
+- **Event handling**: Using `@when` decorator with CSS selectors.
+
+## Comparing with pyscript.web
+
+This is the exact same application as the
+[pyscript.web version](../task-board-web/), but implemented using
+JavaScript APIs directly. Key differences:
+
+### Finding elements
+
+**pyscript.web**: `web.page["tasks"]`
+
+**FFI**: `document.getElementById("tasks")`
+
+### Creating elements
+
+**pyscript.web**:
+```python
+task_div = web.div(
+ checkbox,
+ task_text,
+ delete_btn,
+ classes=["task", priority]
+)
+```
+
+**FFI**:
+```python
+task_div = document.createElement("div")
+task_div.className = f"task {priority}"
+task_div.appendChild(checkbox)
+task_div.appendChild(task_text)
+task_div.appendChild(delete_btn)
+```
+
+### Working with classes
+
+**pyscript.web**: `element.classes.add("selected")`
+
+**FFI**: `element.classList.add("selected")`
+
+### Setting content
+
+**pyscript.web**: `element.innerHTML = "text"`
+
+**FFI**: `element.textContent = "text"` or `element.innerHTML = "text"`
+
+## Which approach to use?
+
+Both work perfectly! The pyscript.web version is more Pythonic and
+concise, whilst the FFI version gives you direct access to JavaScript
+APIs. Choose based on your preference and familiarity with web
+development.
+
+## Files
+
+- `index.html` - Page structure and styling (same as web version).
+- `main.py` - Application logic using FFI.
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/task-board-ffi/main.py b/docs/example-apps/task-board-ffi/main.py
new file mode 100644
index 0000000..717f4b9
--- /dev/null
+++ b/docs/example-apps/task-board-ffi/main.py
@@ -0,0 +1,204 @@
+"""
+Task Board application demonstrating the FFI.
+
+Shows how to use JavaScript APIs directly from Python: querySelector,
+createElement, classList, dataset, and addEventListener. Compare this
+with the pyscript.web version to see the differences.
+"""
+from pyscript import document, when
+
+
+# Track tasks with their DOM elements.
+tasks = []
+current_filter = "all"
+selected_priority = "medium"
+
+
+def update_visibility():
+ """
+ Update visibility of task elements based on current filter.
+ """
+ for task in tasks:
+ if task["deleted"]:
+ task["element"].style.display = "none"
+ continue
+
+ should_show = False
+ if current_filter == "all":
+ should_show = True
+ elif current_filter == "active":
+ should_show = not task["completed"]
+ elif current_filter == "completed":
+ should_show = task["completed"]
+
+ task["element"].style.display = "flex" if should_show else "none"
+
+ # Show empty state if no visible tasks.
+ visible_count = sum(
+ 1 for t in tasks
+ if not t["deleted"] and (
+ current_filter == "all" or
+ (current_filter == "active" and not t["completed"]) or
+ (current_filter == "completed" and t["completed"])
+ )
+ )
+
+ empty_state = document.getElementById("empty-state")
+ if visible_count == 0:
+ if not empty_state:
+ empty = document.createElement("div")
+ empty.id = "empty-state"
+ empty.className = "empty-state"
+ empty.textContent = "No tasks yet. Add one above!"
+ document.getElementById("tasks").appendChild(empty)
+ else:
+ if empty_state:
+ empty_state.remove()
+
+
+def toggle_complete(event):
+ """
+ Toggle task completion status.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["completed"] = event.target.checked
+
+ # Update visual state.
+ task_element = tasks[index]["element"]
+ if event.target.checked:
+ task_element.classList.add("completed")
+ else:
+ task_element.classList.remove("completed")
+
+ # Update visibility based on current filter.
+ update_visibility()
+
+
+def delete_task(event):
+ """
+ Mark a task as deleted.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["deleted"] = True
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("click", "#add-task-btn")
+def add_task(event):
+ """
+ Add a new task when the button is clicked.
+ """
+ task_input = document.getElementById("task-input")
+ text = task_input.value.strip()
+
+ if not text:
+ return
+
+ # Create task object.
+ task_index = len(tasks)
+
+ # Create checkbox.
+ checkbox = document.createElement("input")
+ checkbox.type = "checkbox"
+ checkbox.className = "checkbox"
+ checkbox.checked = False
+ checkbox.dataset.index = str(task_index)
+ checkbox.addEventListener("change", toggle_complete)
+
+ # Create task text.
+ task_text = document.createElement("div")
+ task_text.className = "task-text"
+ task_text.textContent = text
+
+ # Create delete button.
+ delete_btn = document.createElement("button")
+ delete_btn.className = "delete-btn"
+ delete_btn.textContent = "Delete"
+ delete_btn.dataset.index = str(task_index)
+ delete_btn.addEventListener("click", delete_task)
+
+ # Create task container.
+ task_div = document.createElement("div")
+ task_div.className = f"task {selected_priority}"
+ task_div.appendChild(checkbox)
+ task_div.appendChild(task_text)
+ task_div.appendChild(delete_btn)
+
+ # Add to DOM.
+ document.getElementById("tasks").appendChild(task_div)
+
+ # Add task to list.
+ tasks.append({
+ "text": text,
+ "priority": selected_priority,
+ "completed": False,
+ "deleted": False,
+ "element": task_div,
+ "checkbox": checkbox
+ })
+
+ # Clear input.
+ task_input.value = ""
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("keypress", "#task-input")
+def handle_keypress(event):
+ """
+ Add task when Enter is pressed.
+ """
+ if event.key == "Enter":
+ add_task(event)
+
+
+@when("click", ".priority-btn")
+def select_priority(event):
+ """
+ Select a priority level.
+ """
+ global selected_priority
+
+ # Get all priority buttons.
+ priority_btns = document.querySelectorAll(".priority-btn")
+
+ # Remove selected class from all.
+ for btn in priority_btns:
+ btn.classList.remove("selected")
+
+ # Add selected class to clicked button.
+ event.target.classList.add("selected")
+
+ # Update selected priority.
+ selected_priority = event.target.dataset.priority
+
+
+@when("click", ".filter-btn")
+def filter_tasks(event):
+ """
+ Filter tasks by completion status.
+ """
+ global current_filter
+
+ # Get all filter buttons.
+ filter_btns = document.querySelectorAll(".filter-btn")
+
+ # Remove active class from all.
+ for btn in filter_btns:
+ btn.classList.remove("active")
+
+ # Add active class to clicked button.
+ event.target.classList.add("active")
+
+ # Update filter.
+ current_filter = event.target.dataset.filter
+
+ # Update visibility.
+ update_visibility()
+
+
+# Initial setup.
+update_visibility()
\ No newline at end of file
diff --git a/docs/example-apps/task-board-web/index.html b/docs/example-apps/task-board-web/index.html
new file mode 100644
index 0000000..964342a
--- /dev/null
+++ b/docs/example-apps/task-board-web/index.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+ Task Board - pyscript.web
+
+
+
+
+
+ PyScript Task Board
+ Built with pyscript.web
+
+
+
+
+ High
+ Medium
+ Low
+
+
Add Task
+
+
+
+ All
+ Active
+ Completed
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/task-board-web/info.md b/docs/example-apps/task-board-web/info.md
new file mode 100644
index 0000000..1d9b662
--- /dev/null
+++ b/docs/example-apps/task-board-web/info.md
@@ -0,0 +1,57 @@
+# Task Board - pyscript.web Version
+
+A task management application demonstrating the Pythonic `pyscript.web`
+interface for DOM manipulation.
+
+## What it demonstrates
+
+- **Finding elements**: Using `web.page["id"]` and `web.page.find()`.
+- **Creating elements**: Using `web.div()`, `web.button()`, etc.
+- **Modifying attributes**: Setting `innerHTML`, `value`, `dataset`.
+- **Working with classes**: Using `classes.add()`, `classes.remove()`.
+- **Collections**: Iterating over elements with `.find()`.
+- **Event handling**: Using `@when` decorator with web elements.
+
+## Features
+
+- Add tasks with text descriptions.
+- Set priority levels (high, medium, low) with visual indicators.
+- Mark tasks as complete with checkboxes.
+- Filter tasks by status (all, active, completed).
+- Delete tasks.
+- Visual feedback with colours and styles.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Application logic using pyscript.web.
+
+## How it works
+
+1. User enters task text and selects a priority level.
+2. Clicking "Add Task" creates a new task object and re-renders.
+3. Tasks are displayed with priority-based colour coding.
+4. Checkboxes toggle completion status.
+5. Filter buttons show different subsets of tasks.
+6. Delete buttons remove tasks from the list.
+
+All DOM manipulation uses `pyscript.web`'s Pythonic interface:
+- Elements accessed via `web.page["id"]` (dictionary-style).
+- Classes managed with set operations (`add`, `remove`).
+- Elements created with function calls (`web.div()`).
+- Events handled with `@when` decorator.
+
+## Compare with FFI version
+
+See the [FFI version](../task-board-ffi/) of this same application to
+compare the Pythonic approach with direct JavaScript API calls.
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/task-board-web/main.py b/docs/example-apps/task-board-web/main.py
new file mode 100644
index 0000000..0e9a60b
--- /dev/null
+++ b/docs/example-apps/task-board-web/main.py
@@ -0,0 +1,210 @@
+"""
+Task Board application demonstrating pyscript.web.
+
+Shows how to find elements, create elements, manipulate attributes,
+work with classes and styles, and handle events using the Pythonic
+pyscript.web interface.
+"""
+from pyscript import when, web
+
+
+# Track tasks with their DOM elements.
+tasks = []
+current_filter = "all"
+selected_priority = "medium"
+
+
+def update_visibility():
+ """
+ Update visibility of task elements based on current filter.
+ """
+ for task in tasks:
+ if task["deleted"]:
+ task["element"].style["display"] = "none"
+ continue
+
+ should_show = False
+ if current_filter == "all":
+ should_show = True
+ elif current_filter == "active":
+ should_show = not task["completed"]
+ elif current_filter == "completed":
+ should_show = task["completed"]
+
+ task["element"].style["display"] = "flex" if should_show else "none"
+
+ # Show empty state if no visible tasks.
+ visible_count = sum(
+ 1 for t in tasks
+ if not t["deleted"] and (
+ current_filter == "all" or
+ (current_filter == "active" and not t["completed"]) or
+ (current_filter == "completed" and t["completed"])
+ )
+ )
+
+ empty_state = web.page["empty-state"]
+ tasks_container = web.page["tasks"]
+
+ if visible_count == 0:
+ if not empty_state:
+ empty = web.div(
+ "No tasks yet. Add one above!",
+ id="empty-state",
+ classes=["empty-state"]
+ )
+ tasks_container.append(empty)
+ else:
+ if empty_state:
+ empty_state._dom_element.remove()
+
+
+def toggle_complete(event):
+ """
+ Toggle task completion status.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["completed"] = event.target.checked
+
+ # Update visual state.
+ task_element = tasks[index]["element"]
+ if event.target.checked:
+ task_element.classes.add("completed")
+ else:
+ task_element.classes.remove("completed")
+
+ # Update visibility based on current filter.
+ update_visibility()
+
+
+def delete_task(event):
+ """
+ Mark a task as deleted.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["deleted"] = True
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("click", "#add-task-btn")
+def add_task(event):
+ """
+ Add a new task when the button is clicked.
+ """
+ task_input = web.page["task-input"]
+ text = task_input.value.strip()
+
+ if not text:
+ return
+
+ # Create task object.
+ task_index = len(tasks)
+
+ # Create checkbox.
+ checkbox = web.input(
+ type="checkbox",
+ classes=["checkbox"]
+ )
+ checkbox.dataset.index = str(task_index)
+ checkbox._dom_element.addEventListener("change", toggle_complete)
+
+ # Create task text.
+ task_text = web.div(
+ text,
+ classes=["task-text"]
+ )
+
+ # Create delete button.
+ delete_btn = web.button(
+ "Delete",
+ classes=["delete-btn"]
+ )
+ delete_btn.dataset.index = str(task_index)
+ delete_btn._dom_element.addEventListener("click", delete_task)
+
+ # Create task container.
+ task_div = web.div(
+ checkbox,
+ task_text,
+ delete_btn,
+ classes=["task", selected_priority]
+ )
+
+ # Add to DOM.
+ web.page["tasks"].append(task_div)
+
+ # Add task to list.
+ tasks.append({
+ "text": text,
+ "priority": selected_priority,
+ "completed": False,
+ "deleted": False,
+ "element": task_div,
+ "checkbox": checkbox
+ })
+
+ # Clear input.
+ task_input.value = ""
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("keypress", "#task-input")
+def handle_keypress(event):
+ """
+ Add task when Enter is pressed.
+ """
+ if event.key == "Enter":
+ add_task(event)
+
+
+@when("click", ".priority-btn")
+def select_priority(event):
+ """
+ Select a priority level.
+ """
+ global selected_priority
+
+ # Get all priority buttons.
+ priority_btns = web.page.find(".priority-btn")
+
+ # Remove selected class from all.
+ for btn in priority_btns:
+ btn.classes.remove("selected")
+
+ # Add selected class to clicked button.
+ event.target.classes.add("selected")
+
+ # Update selected priority.
+ selected_priority = event.target.dataset.priority
+
+
+@when("click", ".filter-btn")
+def filter_tasks(event):
+ """
+ Filter tasks by completion status.
+ """
+ global current_filter
+
+ # Get all filter buttons.
+ filter_btns = web.page.find(".filter-btn")
+
+ # Remove active class from all.
+ for btn in filter_btns:
+ btn.classes.remove("active")
+
+ # Add active class to clicked button.
+ event.target.classes.add("active")
+
+ # Update filter.
+ current_filter = event.target.dataset.filter
+
+ # Update visibility.
+ update_visibility()
+
+
+# Initial setup.
+update_visibility()
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
index bd8a941..f2a1765 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -1,25 +1,25 @@
-# FAQ
+# Frequently Asked Questions
-This page contains the most common questions and "*gotchas*" asked on
-[our Discord server](https://discord.gg/HxvBtukrg2), in
-[our community calls](https://www.youtube.com/@PyScriptTV), or
-within our community.
+This page addresses common questions and troubleshooting scenarios
+encountered by the PyScript community on
+[Discord](https://discord.gg/HxvBtukrg2), in
+[community calls](https://www.youtube.com/@PyScriptTV), and through
+general usage.
-There are two major areas we'd like to explore:
-[common errors](#common-errors) and [helpful hints](#helpful-hints).
+The FAQ covers two main areas: [common errors](#common-errors) and
+[helpful hints](#helpful-hints).
## Common errors
-### Reading errors
+### Reading error messages
-If your application doesn't run, and you don't see any error messages on the
-page, you should check
+When your application doesn't run and you see no error messages on the
+page, check
[your browser's console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools).
-When reading an error message, the easy way to find out what's going on,
-most of the time, is to read the last line of the error.
+The last line of an error message usually reveals the problem:
-```text title="A Pyodide error."
+```text
Traceback (most recent call last):
File "/lib/python311.zip/_pyodide/_base.py", line 501, in eval_code
.run(globals, locals)
@@ -29,33 +29,28 @@ Traceback (most recent call last):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "", line 1, in
NameError: name 'failure' is not defined
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
-```text title="A MicroPython error."
+```text
Traceback (most recent call last):
File "", line 1, in
NameError: name 'failure' isn't defined
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
-In both examples, the code created a
+Both examples show a
[`NameError`](https://docs.python.org/3/library/exceptions.html#NameError)
-because the object with the name `failure` did not exist. Everything above the
-error message is potentially useful technical detail.
+because the name `failure` doesn't exist. Everything above the error
+message provides potentially useful technical detail for debugging.
-With this context in mind, these are the most common errors users of PyScript
-encounter.
+These are the most common errors PyScript users encounter.
### SharedArrayBuffer
-This is the first and most common error users may encounter with PyScript:
+This is the most common error new PyScript users face:
!!! failure
- Your application doesn't run and in
- [your browser's console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools)
- you see this message:
+ Your application doesn't run and your browser console shows:
```
Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer
@@ -63,72 +58,64 @@ This is the first and most common error users may encounter with PyScript:
#### When
-This happens when you're unable to access objects in the main thread (`window`
-and `document`) from code running in a web worker.
+This error occurs when code running in a worker tries to access `window`
+or `document` objects that exist on the main thread.
-This error happens because **the server delivering your PyScript application is
-incorrectly configured** or **a `service-worker` attribute has not been used in
-your `script` element**.
+The error indicates either **your web server is incorrectly configured**
+or **a `service-worker` attribute is missing from your script element**.
-Specifically, one of the following three problem situations applies to your
-code:
+Specifically, one of three situations applies:
-* Because of the way your web server is configured, the browser limits the use
- of a technology called "Atomics" (you don't need to know how it works, just
- that it may be limited by the browser). If there is a `worker` attribute in
- your `script` element, and your Python code uses the `window` or `document`
- objects (that actually exist on the main thread), then the browser limitation
- on Atomics will cause the failure, unless you reconfigure your server.
-* There is a `
```
-Alternatively, ensure any JavaScript code you reference uses `export ...` or
-ask for an `.mjs` version of the code. All the various options and technical
-considerations surrounding the use of JavaScript modules in PyScript are
-[covered in our user guide](../user-guide/dom/#working-with-javascript).
+Alternatively, ensure referenced JavaScript code uses `export` or
+request an `.mjs` version. The
+[user guide](../user-guide/dom/#working-with-javascript) covers all
+options and technical considerations for using JavaScript modules.
#### Why
-Even though the standard for JavaScript modules has existed since 2015, many
-old and new libraries still produce files that are incompatible with such
-modern and idiomatic standards.
+Although the JavaScript module standard has existed since 2015, many
+libraries still produce files incompatible with modern standards.
-This isn't so much a technical problem, as a human problem as folks learn to
-use the new standard and migrate old code away from previous and now
-obsolete standards.
+This reflects the JavaScript ecosystem's evolution rather than a
+technical limitation. Developers are learning the new standard and
+migrating legacy code from obsolete patterns.
-While such legacy code exists, be aware that JavaScript code may require
-special care.
+While legacy code exists, JavaScript may require special handling.
### Possible deadlock
-Users may encounter an error message similar to the following:
+This error message indicates a serious problem:
!!! failure
@@ -369,912 +339,601 @@ Users may encounter an error message similar to the following:
#### When
-This error happens when your code on a worker and in the main thread are
-[in a deadlock](https://en.wikipedia.org/wiki/Deadlock). Put simply, neither
-fragment of code can proceed without waiting for the other.
+This error occurs when code on a worker and the main thread are in
+[deadlock](https://en.wikipedia.org/wiki/Deadlock). Neither fragment can
+proceed without waiting for the other.
#### Why
-Let's assume a worker script contains the following Python code:
+Consider this worker code:
-```python title="worker: a deadlock example"
+```python
from pyscript import sync
sync.worker_task = lambda: print('🔥 this is fine 🔥')
-# deadlock 💀🔒
+# Deadlock occurs here. 💀🔒
sync.main_task()
```
-On the main thread, let's instead assume this code:
+And this main thread code:
-```html title="main: a deadlock example"
+```html
```
-When the worker bootstraps and calls `sync.main_task()` on the main thread, it
-blocks until the result of this call is returned. Hence it cannot respond to
-anything at all. However, in the code on the main thread, the
-`sync.worker_task()` in the worker is called, but the worker is blocked! Now
-the code on both the main thread and worker are mutually blocked and waiting
-on each other. We are in a classic
-[deadlock](https://en.wikipedia.org/wiki/Deadlock) situation.
-
-The moral of the story? Don't create such circular deadlocks!
+The main thread calls `main_task()`, which awaits `worker_task()` on the
+worker. But `worker_task()` can only execute after `main_task()`
+completes. Neither can proceed - classic deadlock.
-How?
+PyScript detects this situation and raises the error to prevent your
+application from freezing.
-The mutually blocking calls cause the deadlock, so simply don't block.
-
-For example, on the main thread, let's instead assume this code:
-
-```html title="main: avoiding deadlocks"
-
-```
+#### Solution
-By scheduling the call to the worker (rather than awaiting it), it's possible
-for the main thread to call functions defined in the worker in a non-blocking
-manner, thus allowing the worker to also work in an unblocked manner and react
-to such calls. We have resolved the mutual deadlock.
+Restructure your code to avoid circular dependencies between main thread
+and worker. One thread should complete its work before the other begins,
+or they should work independently without waiting for each other.
### TypeError: crypto.randomUUID is not a function
-If PyScript fails to start and you look in the browser console, you may
-find the following error:
+This error appears in specific browser environments:
!!! failure
```
- main.js:43 Uncaught TypeError: crypto.randomUUID is not a function
- at main.js:43:26
+ TypeError: crypto.randomUUID is not a function
```
#### When
-This happens because PyScript uses the `crypto.randomUUID` function, and the
-web page isn't served correctly.
+This occurs when using PyScript in environments where the
+`crypto.randomUUID` API isn't available. This typically happens in:
-#### Why
+Older browsers not supporting this API.
-This error is _created by the browser_ because `crypto.randomUUID` requires a
-secure context or localhost to use the latest web standards that are part of
-PyScript's core (such as `crypto.randomUUID`).
+Non-secure contexts (HTTP instead of HTTPS). The `crypto.randomUUID`
+function requires a secure context.
-Put simply, your code should be served from a domain secured
-with TLS (i.e. the domain name starts with `https` - use a service like
-[let's encrypt](https://letsencrypt.org/) to address this) or from `localhost`
-if developing and viewing your site on your development machine.
+Certain embedded browser environments or WebViews with restricted APIs.
-This is something PyScript can't fix. Rather, it's how the web works and you
-should always ensure your code is served in a secure manner.
+#### Solution
-## Helpful hints
+Use HTTPS for your application. The `crypto` API requires secure
+contexts.
-This section contains common hacks or hints to make using PyScript easier.
+Update to modern browsers supporting the full Web Crypto API.
-!!! Note
+If working in a restricted environment, you may need to polyfill
+`crypto.randomUUID` or use an alternative approach for generating unique
+identifiers.
- We have an absolutely lovely PyScript contributor called
- [Jeff Glass](https://github.com/jeffersglass) who maintains an exceptional
- blog full of [PyScript recipes](https://pyscript.recipes/) with even more
- use cases, hints, tips and solutions. Jeff also has a
- [wonderful YouTube channel](https://www.youtube.com/@CodingGlass) full of
- very engaging PyScript related content.
-
- If you cannot find what you are looking for here, please check Jeff's blog
- as it's likely he's probably covered something close to the situation in
- which you find yourself.
+## Helpful hints
- Of course, if ever you meet Jeff in person, please buy him a beer and
- remember to say a big "thank you". 🍻
+This section provides guidance on common scenarios and best practices.
### PyScript `latest`
-PyScript follows the [CalVer](https://calver.org/) convention for version
-numbering.
-
-Put simply, it means each version is numbered according to when, in the
-calendar, it was released. For instance, version `2024.4.2` was the _second_
-release in the month of April in the year 2024 (**not** the release on the 2nd
-of April but the second release **in** April).
-
-It used to be possible to reference PyScript via a version called `latest`,
-which would guarantee you always got the latest release.
-
-However, at the end of 2023, we decided to **stop supporting `latest` as a
-way to reference PyScript**. We did this for two broad reasons:
-
-1. In the autumn of 2023, we release a completely updated version of PyScript
- with some breaking changes. Folks who wrote for the old version, yet still
- referenced `latest`, found their applications broke. We want to avoid this
- at all costs.
-2. Our release cadence is more regular, with around two or three releases a
- month. Having transitioned to the new version of PyScript, we aim to avoid
- breaking changes. However, we are refining and adding features as we adapt
- to our users' invaluable feedback.
-
-Therefore,
-[pinning your app's version of PyScript to a specific release](https://github.com/pyscript/pyscript/releases)
-(rather than `latest`) ensures you get exactly the version of PyScript you
-used when writing your code.
-
-However, as we continue to develop PyScript it _is_ possible to get our latest
-development version of PyScript via `npm` and we could (should there be enough
-interest) deliver our work-in-progress via a CDN's "canary" or "development"
-channel. **We do not guarantee the stability of such versions of PyScript**,
-so never use them in production, and our documentation may not reflect the
-development version.
-
-If you require the development version of PyScript, these are the URLs to use:
-
-```html title="PyScript development. ⚠️⚠️⚠️ WARNING: HANDLE WITH CARE! ⚠️⚠️⚠️"
-
-
+When including PyScript in your HTML, you can reference specific
+versions or use `latest`:
+
+```html
+
+
+
+
+
```
-!!! warning
+#### Production vs development
+
+For production applications, always use specific version numbers. This
+ensures your application continues working even when new PyScript
+versions are released. Updates happen on your schedule, not
+automatically.
+
+For development and experimentation, `latest` provides convenient access
+to new features without updating version numbers.
- ***Do not use shorter urls or other CDNs.***
+#### Version compatibility
- PyScript needs both the correct headers to use workers and to find its own
- assets at runtime. Other CDN links might result into a **broken
- experience**.
+When reporting bugs or asking questions, always mention which PyScript
+version you're using. Different versions may behave differently, and
+version information helps diagnose problems.
-### Workers via JavaScript
+Check the [releases page](https://pyscript.net/releases/) to see
+available versions and their release notes.
-Sometimes you want to start a Pyodide or MicroPython web worker from
-JavaScript.
+### Workers via JavaScript
-Here's how:
+You can create workers programmatically from JavaScript:
-```html title="Starting a PyScript worker from JavaScript."
+```html
```
-```python title="micro.py"
-from pyscript import sync
+This approach is useful when:
-def do_stuff():
- print("heavy computation")
+You're building primarily JavaScript applications that need Python
+functionality.
-# Note: this reference is awaited in the JavaScript code.
-sync.doStuff = do_stuff
-```
+You want dynamic worker creation based on runtime conditions.
+
+You're integrating PyScript into existing JavaScript frameworks.
+
+The worker runs Python code in a separate thread, keeping your main
+thread responsive. Use `worker.sync` to call Python functions from
+JavaScript, and vice versa through `pyscript.window`.
### JavaScript `Class.new()`
-When using Python to instantiate a class defined in JavaScript, one needs to
-use the class's `new()` method, rather than just using `Class()` (as in
-Python).
-
-Why?
-
-The reason is technical, related to JavaScript's history and its relatively
-poor introspection capabilities:
-
-* In JavaScript, `typeof function () {}` and `typeof class {}` produce the
- same outcome: `function`. This makes it **very hard to disambiguate the
- intent of the caller** as both are valid, JavaScript used to use
- `function` (rather than `class`) to instantiate objects, and the class you're
- using may not use the modern, `class` based, idiom.
-* In the FFI, the JavaScript proxy has traps to intercept the use of the
- `apply` and `construct` methods used during instantiation. However, because
- of the previous point, it's not possible to be sure that `apply` is meant to
- `construct` an instance or call a function.
-* Unlike Python, just invoking a `Class()` in JavaScript (without
- [the `new` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new))
- throws an error.
-* Using `new Class()` is invalid syntax in Python. So there is still a need to
- somehow disambiguate the intent to call a function or instantiate a class.
-* Making use of the capitalized-name-for-classes convention is brittle because
- when JavaScript code is minified the class name can sometimes change.
-* This leaves our convention of `Class.new()` to explicitly signal the intent
- to instantiate a JavaScript class. While not ideal it is clear and
- unambiguous.
+When creating JavaScript class instances from Python, use the `.new()`
+method:
-### PyScript events
+```python
+from pyscript import window
-PyScript uses hooks during the lifecycle of the application to facilitate the
-[creation of plugins](../user-guide/plugins/).
+# Create a new Date instance.
+date = window.Date.new()
-Beside hooks, PyScript also dispatches events at specific moments in the
-lifecycle of the app, so users can react to changes in state:
+# Create other class instances.
+map_instance = window.Map.new()
+set_instance = window.Set.new()
+```
-#### m/py:ready
+This pattern exists because Python's `Date()` would attempt to call the
+JavaScript class as a function rather than constructing an instance.
-Both the `mpy:ready` and `py:ready` events are dispatched for every PyScript
-related element found on the page. This includes `
-
-bootstrapping
-
```
-A classic use case for this event is to recreate the "starting up"
-spinner that used to be displayed when PyScript bootstrapped. Just show the
-spinner first, then close it once `py:ready` is triggered!
+#### Available events
-!!! warning
+**`py:ready`** - Pyodide interpreter is ready and about to run code.
- If using Pyodide on the main thread, the UI will block until Pyodide has
- finished bootstrapping. The "starting up" spinner won't work unless Pyodide
- is started on a worker instead.
-
-#### m/py:done
-
-The `mpy:done` and `py:done` events dispatch after the either the synchronous
-or asynchronous code has finished execution.
-
-```html title="A py:done example."
-
+**`mpy:ready`** - MicroPython interpreter is ready and about to run
+code.
-
-bootstrapping
-
-```
+**`py:done`** - All Pyodide scripts have finished executing.
-!!! warning
+**`mpy:done`** - All MicroPython scripts have finished executing.
- If `async` code contains an infinite loop or some orchestration that keeps
- it running forever, then these events may never trigger because the code
- never really finishes.
+**`py:all-done`** - All PyScript activity has completed.
-#### py:all-done
+#### Event details
-The `py:all-done` event dispatches when all code is finished executing.
+Events carry useful information in their `detail` property:
-This event is special because it depends upon all the MicroPython and Pyodide
-scripts found on the page, no matter the interpreter.
+```javascript
+document.addEventListener('py:ready', (event) => {
+ // Access the script element.
+ const script = event.detail.script;
+
+ // Access the interpreter wrapper.
+ const wrap = event.detail.wrap;
+});
+```
-In this example, MicroPython waves before Pyodide before the `"everything is
-done"` message is written to the browser's console.
+Use these events to:
-```html title="A py:all-done example."
-
-
-
-```
+Show loading indicators whilst Python initialises.
-#### m/py:progress
+Coordinate between JavaScript and Python code.
-The `py:progress` or `mpy:progress` event triggers on the main thread *during*
-interpreter bootstrap (no matter if your code is running on main or in a
-worker).
+Enable UI elements only after Python is ready.
-The received `event.detail` is a string that indicates operations between
-`Loading {what}` and `Loaded {what}`. So, the first event would be, for
-example, `Loading Pyodide` and the last one per each bootstrap would be
-`Loaded Pyodide`.
+Track application lifecycle for debugging or analytics.
-In between all operations are `event.detail`s, such as:
+### Packaging pointers
- * `Loading files` and `Loaded files`, when `[files]` is found in the optional
- config
- * `Loading fetch` and `Loaded fetch`, when `[fetch]` is found in the optional
- config
- * `Loading JS modules` and `Loaded JS modules`, when `[js_modules.main]` or
- `[js_modules.worker]` is found in the optional config
- * finally, all optional packages handled via *micropip* or *mip* will also
- trigger various `Loading ...` and `Loaded ...` events so that users can see
- what is going on while PyScript is bootstrapping
+Understanding packaging helps you use external Python libraries
+effectively.
-An example of this listener applied to a dialog can be [found in here](https://agiammarchi.pyscriptapps.com/kmeans-in-panel-copy/v1/).
+#### Pyodide packages
-### Packaging pointers
+Pyodide includes many pre-built packages. Check the
+[package list](https://pyodide.org/en/stable/usage/packages-in-pyodide.html)
+to see what's available.
+
+Install pure Python packages from PyPI using `micropip`:
+
+```python
+import micropip
-Applications need third party packages and [PyScript can be configured to
-automatically install packages for you](user-guide/configuration/#packages).
-Yet [packaging can be a complicated beast](#python-packages), so here are some
-hints for a painless packaging experience with PyScript.
-
-There are essentially five ways in which a third party package can become
-available in PyScript.
-
-1. The module is already part of either the Pyodide or MicroPython
- distribution. For instance, Pyodide includes numpy, pandas, scipy,
- matplotlib and scikit-learn as pre-built packages you need only activate
- via the [`packages` setting](../user-guide/configuration/#packages) in
- PyScript. There are plans for MicroPython to offer different builds for
- PyScript, some to include MicroPython's version of numpy or the API for
- sqlite.
-2. Host a standard Python package somewhere (such as
- [PyScript.com](https://pyscript.com) or in a GitHub repository) so it can
- be fetched as a package via a URL at runtime.
-3. Reference hosted Python source files, to be included on the file
- system, via the [`files` setting](../user-guide/configuration/#files).
-4. Create a folder containing the package's files and sub folders, and create
- a hosted `.zip` or `.tgz`/`.tar.gz`/`.whl` archive to be decompressed into
- the file system (again, via the
- [`files` setting](../user-guide/configuration/#files)).
-5. Provide your own `.whl` package and reference it via a URL in the
- `packages = [...]` list.
-
-#### Host a package
-
-Just put the package you need somewhere it can be served (like
-[PyScript.com](https://pyscript.com/)) and reference the URL in the
-[`packages` setting](../user-guide/configuration/#packages). So long as the
-server at which you are hosting the package
-[allows CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
-(fetching files from other domains) everything should just work.
-
-It is even possible to install such packages at runtime, as this example using
-MicroPython's [`mip` tool](https://docs.micropython.org/en/latest/reference/packages.html)
-demonstrates (the equivalent can be achieved with Pyodide
-[via `micropip`](https://micropip.pyodide.org/en/stable/)).
-
-```python title="MicroPython mip example."
-# Install default version from micropython-lib
-mip.install("keyword")
-
-# Install from raw URL
-mip.install("https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/bisect/bisect.py")
-
-# Install from GitHub shortcut
-mip.install("github:jeffersglass/some-project/foo.py")
+await micropip.install("pillow")
```
-#### Provide your own file
+Some packages with C extensions are available. If `micropip` reports it
+cannot find a pure Python wheel, the package either:
-One can use the [`files` setting](../user-guide/configuration/#files) to copy
-packages onto the Python path:
+Contains C extensions not compiled for WebAssembly.
-```html title="A file copied into the Python path."
-
-[files]
-"./modules/bisect.py" = "./bisect.py"
-
-
-```
+Isn't compatible with the browser environment.
-#### Code archive (`zip`/`tgz`/`whl`)
+Has dependencies that aren't available.
-Compress all the code you want into an archive (using either either `zip` or
-`tgz`/`tar.gz`). Host the resulting archive and use the
-[`files` setting](../user-guide/configuration/#files) to decompress it onto
-the Python interpreter's file system.
+#### MicroPython packages
-Consider the following file structure:
+MicroPython uses packages from
+[micropython-lib](https://github.com/micropython/micropython-lib).
+Reference them in configuration:
-```
-my_module/__init__.py
-my_module/util.py
-my_module/sub/sub_util.py
+```toml
+[packages]
+"unittest" = ""
```
-Host it somewhere, and decompress it into the home directory of the Python
-interpreter:
+For packages not in micropython-lib, use the `files` configuration to
+include them:
-```html title="A code archive."
-
+```toml
[files]
-"./my_module.zip" = "./*"
-
+"my_package.py" = "https://example.com/my_package.py"
+```
-
+Or reference local files:
+
+```toml
+[files]
+"my_package.py" = "./my_package.py"
```
-Please note, the target folder must end with a star (`*`), and will contain
-everything in the archive. For example, `"./*"` refers to the home folder for
-the interpreter.
+#### Package size considerations
+
+Pyodide packages can be large. The `numpy` package alone is several
+megabytes. Consider:
-### File System
+Using MicroPython for applications where package access isn't critical.
-Python expects a file system. In PyScript each interpreter provides its own
-in-memory **virtual** file system. **This is not the same as the filesystem
-on the user's device**, but is simply a block of memory in the browser.
+Loading only necessary packages.
-!!! warning
+Showing loading indicators whilst packages download.
- **The file system is not persistent nor shareable** (yet).
+Caching packages for offline use in production applications.
- Every time a user loads or stores files, it is done in ephemeral memory
- associated with the current browser session. Beyond the life of the
- session, nothing is shared, nothing is stored, nothing persists!
+### Filesystem
-#### Read/Write
+PyScript provides virtual filesystems through Emscripten. Understanding
+how they work helps you manage files effectively.
-The easiest way to add content to the virtual file system is by using native
-Python file operations:
+#### Virtual filesystem basics
-```python title="Writing to a text file."
-with open("./test.txt", "w") as dest:
- dest.write("hello vFS")
- dest.close()
+Both Pyodide and MicroPython run in sandboxed environments with virtual
+filesystems. These aren't the user's actual filesystem - they're
+in-memory or browser-storage-backed filesystems provided by Emscripten.
-# Read and print the written content.
-with open("./test.txt", "r") as f:
- content = f.read()
- print(content)
+Files you create or modify exist only in this virtual environment. They
+persist during the session but may not survive page reloads unless
+explicitly saved to browser storage.
+
+#### Loading files
+
+Use the `files` configuration to make files available:
+
+```toml
+[files]
+"data.csv" = "./data.csv"
+"config.json" = "https://example.com/config.json"
```
-Combined with our `pyscript.fetch` utility, it's also possible to store more
-complex data from the web.
+PyScript downloads these files and places them in the virtual
+filesystem. Your Python code can then open them normally:
-```python title="Writing a binary file."
-# Assume async execution.
-from pyscript import fetch, window
+```python
+with open("data.csv") as f:
+ data = f.read()
+```
-href = window.location.href
+#### Writing files
-with open("./page.html", "wb") as dest:
- dest.write(await fetch(href).bytearray())
+You can create and write files in the virtual filesystem:
-# Read and print the current HTML page.
-with open("./page.html", "r") as source:
- print(source.read())
+```python
+with open("output.txt", "w") as f:
+ f.write("Hello, world!")
```
-#### Upload
+These files exist in memory. To provide them for download, use the
+browser's download mechanism:
-It's possible to upload a file onto the virtual file system from the browser
-(` `), and using the DOM API.
+```python
+from pyscript import window, ffi
-The following fragment is just one way to achieve this. It's very simple and
-builds on the file system examples already seen.
-```html title="Upload files onto the virtual file system via the browser."
-
-
+def download_file(filename, content):
+ """
+ Trigger browser download of file content.
+ """
+ blob = window.Blob.new([content], ffi.to_js({"type": "text/plain"}))
+ url = window.URL.createObjectURL(blob)
+
+ link = window.document.createElement("a")
+ link.href = url
+ link.download = filename
+ link.click()
+
+ window.URL.revokeObjectURL(url)
-
-
+
+# Use it.
+download_file("output.txt", "File contents here")
```
-#### Download
+#### Browser storage
-It is also possible to create a temporary link through which you can download
-files present on the interpreter's virtual file system.
+For persistent storage across sessions, use browser storage APIs:
+```python
+from pyscript import window
-```python title="Download file from the virtual file system."
-from pyscript import document, ffi, window
-import os
+# Save data to localStorage.
+window.localStorage.setItem("key", "value")
-def download_file(path, mime_type):
- name = os.path.basename(path)
- with open(path, "rb") as source:
- data = source.read()
+# Retrieve data.
+value = window.localStorage.getItem("key")
+```
+
+Or use the File System Access API for actual file access (requires user
+permission):
- # Populate the buffer.
- buffer = window.Uint8Array.new(len(data))
- for pos, b in enumerate(data):
- buffer[pos] = b
- details = ffi.to_js({"type": mime_type})
+```python
+from pyscript import window
- # This is JS specific
- file = window.File.new([buffer], name, details)
- tmp = window.URL.createObjectURL(file)
- dest = document.createElement("a")
- dest.setAttribute("download", name)
- dest.setAttribute("href", tmp)
- dest.click()
- # here a timeout to window.URL.revokeObjectURL(tmp)
- # should keep the memory clear for the session
+# Request file picker (modern browsers only).
+file_handle = await window.showSaveFilePicker()
+writable = await file_handle.createWritable()
+await writable.write("content")
+await writable.close()
```
### create_proxy
-The `create_proxy` function is described in great detail
-[on the FFI page](../user-guide/ffi/), but it's also useful to explain _when_
-`create_proxy` is needed and the subtle differences between Pyodide and
-MicroPython.
-
-#### Background
+The `create_proxy` function manages Python-JavaScript reference
+lifecycles.
-To call a Python function from JavaScript, the native Python function needs
-to be wrapped in a JavaScript object that JavaScript can use. This JavaScript
-object converts and normalises arguments passed into the function before
-handing off to the native Python function. It also reverses this process with
-any results from the Python function, and so converts and normalises values
-before returning the result to JavaScript.
+#### When to use create_proxy
-The JavaScript primitive used for this purpose is the
-[Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
-It enables "traps", such as
-[apply](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply),
-so the extra work required to call the Python function can happen.
+In Pyodide on the main thread, wrap Python functions passed as
+JavaScript callbacks:
-Once the `apply(target, self, args)` trap is invoked:
+```python
+from pyscript import ffi, window
-* JavaScript must find the correct Python interpreter to evaluate the code.
-* In JavaScript, the `self` argument for `apply` is probably ignored for most
- common cases.
-* All the `args` must be resolved and converted into their Python primitive
- representations or associated Python objects.
-Ultimately, the targets referenced in the `apply` **must exist** in the Python
-context so they are ready when the JavaScript `apply` method calls into the
-Python context.
+def callback(event):
+ """
+ Handle events.
+ """
+ print(event.type)
-**Here's the important caveat**: locally scoped Python functions, or functions
-created at run time cannot be retained forever.
-```python title="A basic Python to JavaScript callback."
-import js
-
-js.addEventListener(
- "custom:event",
- lambda e: print(e.type)
-)
+# Create proxy before passing to JavaScript.
+window.addEventListener("click", ffi.create_proxy(callback))
```
-In this example, the anonymous `lambda` function has no reference in the Python
-context. It's just delegated to the JavaScript runtime via `addEventListener`,
-and then Python immediately garbage collects it. However, as previously
-mentioned, such a Python object must exist for when the `custom:event` is
-dispatched.
+#### When create_proxy isn't needed
-Furthermore, there is no way to define how long the `lambda` should be kept
-alive in the Python environment, nor any way to discover if the `custom:event`
-callback will ever dispatch (so the `lambda` is forever pending). PyScript, the
-browser and the Python interpreters can only work within a finite amount of
-memory, so memory management and the "aliveness" of objects is important.
+In workers, PyScript automatically manages references. You don't need
+`create_proxy`:
-Therefore, `create_proxy` is provided to delegate responsibility for the
-lifecycle of an object to the author of the code. In other words, wrapping the
-`lambda` in a call to `create_proxy` would ensure the Python interpreter
-retains a reference to the anonymous function for future use.
-
-!!! info
+```python
+from pyscript import window
- This probably feels strange! An implementation detail of how the Python
- and JavaScript worlds interact with each other is bleeding into your code
- via `create_proxy`. Surely, if we always just need to create a proxy, a
- more elegant solution would be to do this automatically?
- As you'll see, this is a complicated situation with inevitable tradeoffs,
- but ultimately, through the
- [`experimental_create_proxy = "auto"` flag](../user-guide/configuration/#experimental_create_proxy),
- you probably never need to use `create_proxy`. This section of
- our docs gives you the context you need to make an informed decision.
+def callback(event):
+ """
+ Handle events in worker.
+ """
+ print(event.type)
-**However**, this isn't the end of the story.
-When a Python callback is attached to a specific JavaScript
-instance (rather than passed as argument into an event listener), it is easy
-for the Python interpreter to know when the function could be freed from the
-memory.
+# No create_proxy needed in workers.
+window.addEventListener("click", callback)
+```
-```python title="A sticky lambda."
-from pyscript import document
+With `experimental_create_proxy = "auto"` in configuration, PyScript
+automatically wraps functions:
-# logs "click" if nothing else stopped propagation
-document.onclick = lambda e: print(e.type)
+```toml
+[experimental_create_proxy]
+auto = true
```
-"**Wait, wat? This doesn't make sense at all!?!?**", is a valid
-question/response to this situation.
+```python
+from pyscript import window
-In this case there's
-no need to use `create_proxy` because the JavaScript reference to which the
-function is attached isn't going away and the interpreter can use the
-[`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
-to destroy the `lambda` (or decrease its reference count) when the underlying
-JavaScript reference to which it is attached is itself destroyed.
-#### In Pyodide
+def callback(event):
+ """
+ Handle events with auto proxying.
+ """
+ print(event.type)
-The `create_proxy` utility was created
-([among others](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#module-pyodide.ffi.wrappers))
-to smooth out and circumvent the afore mentioned memory issues when using
-Python callables with JavaScript event handlers.
-Using it requires special care. The coder must invoke the `destroy()` method
-when the Python callback is no longer needed. It means coders must track the
-callback's lifecycle. But this is not always possible:
+# No create_proxy needed with auto mode.
+window.addEventListener("click", callback)
+```
-* If the callback is passed into opaque third party libraries, the reference is
- "lost in a limbo" where who-knows-when the reference should be freed.
-* If the callback is passed to listeners, timers or promises it's hard to
- predict when the callback is no longer needed.
+#### In MicroPython
-Luckily the `Promise` use case is automatically handled by Pyodide, but we're
-still left with the other cases:
+MicroPython creates proxies automatically. The `create_proxy` function
+exists for code portability between Pyodide and MicroPython, but it's
+just a pass-through in MicroPython:
-```python title="Different Pyodide create_proxy contexts."
+```python
from pyscript import ffi, window
-# The create_proxy is needed when a Python
-# function isn't attached to an object reference
-# (but is, rather, an argument passed into
-# the JavaScript context).
-
-# This is needed so a proxy is created for
-# future use, even if `print` won't ever need
-# to be freed from the Python runtime.
-window.setTimeout(
- ffi.create_proxy(print),
- 100,
- "print"
-)
-
-# This is needed because the lambda is
-# immediately garbage collected.
-window.setTimeout(
- ffi.create_proxy(
- lambda x: print(x)
- ),
- 100,
- "lambda"
-)
-def print_type(event):
+def callback(event):
+ """
+ Handle events.
+ """
print(event.type)
-# This is needed even if `print_type`
-# is not a scoped / local function.
-window.addEventListener(
- "some:event",
- ffi.create_proxy(print_type),
- # despite this intent, the proxy
- # will be trapped forever if not destroyed
- ffi.to_js({"once": True})
-)
-# This does NOT need create_function as it is
-# attached to an object reference, hence observed to free.
-window.Object().no_create_function = lambda: print("ok")
+# Works with or without create_proxy in MicroPython.
+window.addEventListener("click", callback)
+window.addEventListener("click", ffi.create_proxy(callback))
```
-To simplify this complicated situation PyScript has an
-`experimental_create_proxy = "auto"` flag. When set, **PyScript intercepts
-JavaScript callback invocations, such as those in the example code above, and
-automatically proxies and destroys any references that are garbage collected
-in the JavaScript environment**.
+Both versions work identically in MicroPython.
-**When this flag is set to `auto` in your configuration, you should never need
-to use `create_proxy` with Pyodide**.
+#### Manual proxy destruction
-!!! Note
+If manually managing proxies in Pyodide, destroy them when done:
- When it comes code running on a web worker, due to the way browser work, no
- Proxy can survive a round trip to the main thread and back.
+```python
+from pyscript import ffi, window
- In this scenario PyScript works differently and references callbacks
- via a unique id, rather than by their identity on the worker. When running
- on a web worker, PyScript automatically frees proxy object references, so
- you never need to use `create_proxy` when running code on a web worker.
-#### In MicroPython
+def callback(event):
+ """
+ One-time handler.
+ """
+ print(event.type)
-The proxy situation is definitely simpler in MicroPython. It just creates
-proxies automatically (so there is no need for a manual `create_proxy` step).
-This is because MicroPython doesn't (yet) have a `destroy()` method for
-proxies, rendering the use case of `create_proxy` redundant.
+proxy = ffi.create_proxy(callback)
+window.addEventListener("click", proxy, ffi.to_js({"once": True}))
-Accordingly, **the use of `create_proxy` in MicroPython is only needed for
-code portability purposes** between Pyodide and MicroPython. When using
-`create_proxy` in MicroPython, it's just a pass-through function and doesn't
-actually do anything.
+# After the event fires once, destroy the proxy.
+# (In practice, the "once" option auto-removes it, but this shows the
+# pattern for cases where you manage lifecycle manually.)
+proxy.destroy()
+```
-All the examples that require `create_proxy` in Pyodide, don't need it in
-MicroPython:
+Manual destruction prevents memory leaks when callbacks are no longer
+needed.
-```python title="Different MicroPython create_proxy contexts."
-from pyscript import window
+### to_js
-# This just works.
-window.setTimeout(print, 100, "print")
+The `to_js` function converts Python objects to JavaScript equivalents.
-# This also just works.
-window.setTimeout(lambda x: print(x), 100, "lambda")
+#### Python dicts to JavaScript objects
-def print_type(event):
- print(event.type)
+Python dictionaries convert to JavaScript object literals, not Maps:
-# This just works too.
-window.addEventListener(
- "some:event",
- print_type,
- ffi.to_js({"once": True})
-)
+```python
+from pyscript import ffi, window
+
+
+config = {"async": False, "cache": True}
+
+# Converts to JavaScript object literal.
+js_config = ffi.to_js(config)
-# And so does this.
-window.Object().no_create_function = lambda: print("ok")
+# Pass to JavaScript APIs expecting objects.
+window.someAPI(js_config)
```
-### to_js
+This differs from Pyodide's default behaviour (which creates Maps).
+PyScript's `to_js` always creates object literals for better JavaScript
+compatibility.
-Use of the `pyodide.ffi.to_js` function is described
-[in the ffi page](../user-guide/ffi/#to_js).
-But it's also useful to cover the *when* and *why* `to_js` is needed, if at
-all.
+#### When to use to_js
-#### Background
+Use `to_js` when passing Python data structures to JavaScript APIs:
-Despite their apparent similarity,
-[Python dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries)
-and
-[JavaScript object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects)
-are very different primitives:
+```python
+from pyscript import ffi, window
-```python title="A Python dictionary."
-ref = {"some": "thing"}
-# Keys don't need quoting, but only when initialising a dict...
-ref = dict(some="thing")
-```
+# Passing configuration objects.
+options = {"method": "POST", "headers": {"Content-Type": "application/json"}}
+window.fetch("/api", ffi.to_js(options))
-```js title="A JavaScript object literal."
-const ref = {"some": "thing"};
+# Passing arrays.
+numbers = [1, 2, 3, 4, 5]
+window.console.log(ffi.to_js(numbers))
-// Keys don't need quoting, so this is as equally valid...
-const ref = {some: "thing"};
+# Passing nested structures.
+data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
+window.processData(ffi.to_js(data))
```
-In both worlds, accessing `ref["some"]` would produce the same result: the
-string `"thing"`.
-
-However, in JavaScript `ref.some` (i.e. a dotted reference to the key) would
-also work to return the string `"thing"` (this is not the case in Python),
-while in Python `ref.get("some")` achieves the same result (and this is not the
-case in JavaScript).
-
-Perhaps because of this, Pyodide chose to convert Python dictionaries to
-JavaScript
-[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
-objects that share a
-[`.get` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get)
-with Python.
-
-Unfortunately, in idiomatic JavaScript and for the vast majority of APIs,
-an object literal (rather than a `Map`) is used to represent key/value pairs.
-Feedback from our users indicates the dissonance of using a `Map` rather than
-the expected object literal to represent a Python `dict` is the source of a
-huge amount of frustration. Sadly, the APIs for `Map` and object literals
-are sufficiently different that one cannot be a drop in replacement for
-another.
-
-Pyodide have provided a way to override the default `Map` based behaviour, but
-this results some rather esoteric code:
-
-```python title="Convert a dict to an object literal in Pyodide."
-import js
-from pyodide.ffi import to_js
-
-js.callback(
- to_js(
- {"async": False},
- # Transform the default Map into an object literal.
- dict_converter=js.Object.fromEntries
- )
-)
-```
-!!! info
+#### Important caveat
- Thanks to a
- [recent change in Pyodide](https://github.com/pyodide/pyodide/pull/4576),
- such `Map` instances are
- [duck-typed](https://en.wikipedia.org/wiki/Duck_typing) to behave like
- object literals. Conversion may not be needed anymore, and `to_js` may just
- work without the need of the `dict_converter`. Please check.
+!!! warning
-MicroPython's version of `to_js` takes the opposite approach (for
-many of the reasons stated above) and converts Python dictionaries to object
-literals instead of `Map` objects.
+ Objects created by `to_js` are detached from the original Python
+ object. Changes to the JavaScript object don't affect the Python
+ object:
+
+ ```python
+ from pyscript import ffi, window
+
+ python_dict = {"key": "value"}
+ js_object = ffi.to_js(python_dict)
+
+ # Modify JavaScript object.
+ js_object.key = "new value"
+
+ # Python dict unchanged.
+ print(python_dict["key"]) # Still "value"
+ ```
-As a result, **the PyScript `pyscript.ffi.to_js` ALWAYS returns a JavaScript
-object literal by default when converting a Python dictionary** no matter if
-you're using Pyodide or MicroPython as your interpreter. Furthermore, when
-using MicroPython, because things are closer to idiomatic JavaScript behaviour,
-you may not even need to use `to_js` unless you want to ensure
-cross-interpreter compatibility.
+This detachment is usually desirable - you're passing data to
+JavaScript, not sharing mutable state. But be aware of this behaviour.
-#### Caveat
+#### MicroPython differences
-!!! warning
+MicroPython's `to_js` already creates object literals by default. You
+may not need `to_js` in MicroPython unless ensuring cross-interpreter
+compatibility:
+
+```python
+from pyscript import window
- **When using `pyscript.to_js`, the result is detached from the original
- Python dictionary.**
+# Works in MicroPython without to_js.
+config = {"async": False}
+window.someAPI(config)
-Any change to the JavaScript object **will not be reflected in the original
-Python object**. For the vast majority of use cases, this is a desirable
-trade-off. But it's important to note this detachment.
+# But using to_js ensures Pyodide compatibility.
+from pyscript import ffi
+window.someAPI(ffi.to_js(config))
+```
-If you're simply passing data around, `pyscript.ffi.to_js` will fulfil your
-requirements in a simple and idiomatic manner.
+For code that might run with either interpreter, use `to_js`
+consistently.
\ No newline at end of file
diff --git a/docs/user-guide/architecture.md b/docs/user-guide/architecture.md
index be311b6..4fb4fdf 100644
--- a/docs/user-guide/architecture.md
+++ b/docs/user-guide/architecture.md
@@ -1,301 +1,202 @@
-# Architecture, Lifecycle & Interpreters
-
-## Core concepts
-
-PyScript's architecture has three core concepts:
-
-1. A small, efficient and powerful kernel called
- [PolyScript](https://github.com/pyscript/polyscript) is the foundation
- upon which PyScript and plugins are built.
-2. A library called [coincident](https://github.com/WebReflection/coincident#readme)
- that simplifies and coordinates interactions with web workers.
-3. The PyScript [stack](https://en.wikipedia.org/wiki/Solution_stack) inside
- the browser is simple and clearly defined.
-
-### PolyScript
-
-[PolyScript](https://github.com/pyscript/polyscript) is the core of PyScript.
-
-!!! danger
-
- Unless you are an advanced user, you only need to know that PolyScript
- exists, and it can be safely ignored.
-
-PolyScript's purpose is to bootstrap the platform and provide all the necessary
-core capabilities. Setting aside PyScript for a moment, to use
-*just PolyScript* requires a `
-
-
-
-
-
-