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" - -``` +## 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.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." - -``` - -```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 ` @@ -145,17 +146,13 @@ the `` 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 ` +

@@ -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=%zseLtPBz>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
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+
+ +
+ + + + + + +
+ +
+

Recent Colours

+
+
+
+ + + + \ 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

+
+ +
+ +
+

HTML Content

+
+ +
+ +
+

Custom Objects

+
+ +
+ +
+

Multiple Values

+
+ +
+ +
+

Data Cards

+
+ +
+ +
+

Incremental 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 += "" + + # Rows. + for i, row in enumerate(self.rows): + bg = "#f8f9fa" if i % 2 == 0 else "white" + html += f"" + for cell in row: + html += f"" + html += "" + + html += "
" + html += f"{header}
{cell}
" + 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(""" +
+

📊 Styled content box

+
+ """), 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.

+ +
+ + +
+ + + +
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.

+ +
+ + +
+ +
+
+

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...

+ + +
+ + + \ 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

+ +
+ + + + +
+ +
+ +
+ +
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

+ +
+ +
+ + + +
+ +
+ +
+ + + +
+ +
+ + + + \ 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

+ +
+ +
+ + + +
+ +
+ +
+ + + +
+ +
+ + + + \ 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 ` - - - - - - -``` +# Architecture -!!! warning +Understanding how PyScript works helps you build better applications and +debug problems when they arise. This guide explains PyScript's +architecture, lifecycle, and the interpreters that power your Python +code in the browser. - **PolyScript is not PyScript.** - - PyScript enhances the available Python interpreters with convenient - features, helper functions and easy-to-use yet powerful capabilities. - - These enhancements are missing from PolyScript. - -PolyScript's capabilities, upon which PyScript is built, can be summarised as: - -* Evaluation of code via [` +```HTML title="Reference a configuration file." + ``` If you use JSON, you can make it the value of the `config` attribute: @@ -83,24 +83,31 @@ _single_ `` or `` tag in your HTML document: !!! warning - Starting from PyScript 2025.10.1 the config guards against incompatible or not - available packages related to the current Pyodide version. + Starting from PyScript 2025.10.1 the config guards against incompatible or unavailable + packages relating to the current Pyodide version. - There is an ongoing effort to make chosing packages in Pyodide way more helpful - than it has ever been but right now be aware if a non existent package for a - specific Pyodide version cannot be found an error will be thrown. + There is an + [ongoing effort to make choosing packages in Pyodide far more helpful](https://pyscript.github.io/pyscript-packages/) + than it has ever been, but right now be aware that if a non-existent package for a + specific Pyodide version cannot be found, an error will be thrown. ## Options -There are five core options ([`interpreter`](#interpreter), [`files`](#files), -[`packages`](#packages), [`js_modules`](#javascript-modules) and -[`sync_main_only`](#sync_main_only)) and two experimental flags: +There are only a few core options ([`interpreter`](#interpreter), [`files`](#files), +[`packages`](#packages), [`js_modules`](#javascript-modules), [`debug`](#debug), +[`sync_main_only`](#sync_main_only)) and some experimental flags: ([`experimental_create_proxy`](#experimental_create_proxy)) that can be used in -the configuration of PyScript and +the configuration of PyScript, ([`experimental_ffi_timeout`](#experimental_ffi_timeout)) to +help with worker related performance, and ([`experimental_remote_packages`](#experimental_remote_packages)) which allows remotely hosted packages. The user is also free to define arbitrary additional configuration options that plugins or an app may require for their own reasons. +!!! info + + You can always access the current configuration as a read-only Python dict via the + `pyscript.config` object. + ### Interpreter The `interpreter` option pins the Python interpreter to the version of the @@ -138,7 +145,7 @@ version of Pyodide as specified in the previous examples: The `files` option fetches arbitrary content from URLs onto the virtual filesystem available to Python, and emulated by the browser. Just map a valid URL to a destination filesystem path on the in-browser virtual filesystem. You -can find out more in the section about +can find out more about the in-browser virtual filesystem in the section about [PyScript and filesystems](../filesystem/). The following JSON and TOML are equivalent: @@ -167,7 +174,7 @@ examples could be equivalently re-written as: { "files": { "https://example.com/data.csv": "", - "./code.py": "" + "./code.py": "./subdir/code.py" } } ``` @@ -175,7 +182,7 @@ examples could be equivalently re-written as: ```toml title="TOML implied filename in the root directory." [files] "https://example.com/data.csv" = "" -"./code.py" = "" +"./code.py" = "./subdir/code.py" ``` If the source part of the configuration is either a `.zip` or `.tar.gz` file @@ -191,18 +198,16 @@ into the target directory in the browser's built in file system. the problem. !!! warning - **Use destination URLs instead of CORS / redirect URLs.** - For example, `https://github.com/pyscript/ltk/raw/refs/heads/main/ltk/jquery.py` - redirects to `https://raw.githubusercontent.com/pyscript/ltk/refs/heads/main/ltk/jquery.py`. Use the latter. - -!!! tip + **Use absolute destination URLs instead of CORS / redirect URLs.** - **For most people, most of the time, the simple URL to filename mapping, - described above, will be sufficient.** + For example, `https://github.com/pyscript/ltk/raw/refs/heads/main/ltk/jquery.py` + redirects to `https://raw.githubusercontent.com/pyscript/ltk/refs/heads/main/ltk/jquery.py`. + + Use the latter. - Yet certain situations may require more flexibility. In which case, read - on. +For most people, most of the time, the simple URL to filename mapping, +described above, will be sufficient. Yet certain situations may require more flexibility. Sometimes many resources are needed to be fetched from a single location and copied into the same directory on the file system. To aid readability and @@ -328,6 +333,16 @@ following valid forms: * An arbitrary URL to a Python package: `"https://.../package.whl"` * A file copied onto the browser based file system: `"emfs://.../package.whl"` +#### Package Support + +We have created a [handy website](https://pyscript.github.io/pyscript-packages/) +that tracks which third party packages in PyPI are supported by PyScript. You +can also use this site to report if a package you're trying to use is supported +or not. + +The [website's help section](https://pyscript.github.io/pyscript-packages/help/) +explains more. + #### Package Cache For performance reasons, PyScript caches packages so that a delay resulting @@ -378,18 +393,15 @@ plugins = ["custom_plugin", "!error"] !!! warning Please note `plugins` are currently a *core* only feature. If you need any - extra functionality out of the box *files* or *js_modules* are the current - way to provide more features without needing to file a *PR* in *core*. - - This means that the current `plugins` proposal is meant to disable our own - plugins but it has no usage to add 3rd party plugins right now. + extra functionality out of the box, `files` or `js_modules` are the current + way to provide more features without needing to file a PR in `core` PyScript. ### JavaScript modules It's easy to import and use JavaScript modules in your Python code. This section of the docs examines the configuration needed to make this work. How to make use of JavaScript is dealt with -[elsewhere](../dom/#working-with-javascript). +[elsewhere](dom.md#working-with-javascript). We need to tell PyScript about the JavaScript modules you want to use. This is the purpose of the `js_modules` related configuration fields. @@ -489,7 +501,7 @@ without the need for the worker to interact with the main thread. You're simply awaiting the result of a method exposed from a worker. This has the advantage of not requiring the use of `SharedArrayBuffer` and -[associated CORS related header configuration](../workers/#http-headers). +[associated CORS related header configuration](workers.md#http-headers). If the `sync_main_only` flag is set, then **interactions between the main thread and workers are limited to one way calls from the main thread to methods @@ -520,7 +532,7 @@ If `sync_main_only` is set, the following caveats apply: Knowing when to use the `pyscript.ffi.create_proxy` method when using Pyodide can be confusing at the best of times and full of -[technical "magic"](../ffi#create_proxy). +[technical "magic"](ffi.md#create_proxy). This _experimental_ flag, when set to `"auto"` will cause PyScript to try to automatically handle such situations, and should "just work". @@ -624,3 +636,83 @@ print(config.get("files")) Changing the `config` dictionary at runtime doesn't change the actual configuration. + +## Practical example + +Here's a complete example showing common configuration options: + +```json title="pyscript.json - A typical configuration." +{ + "packages": [ + "numpy", + "matplotlib" + ], + "files": { + "https://example.com/data.csv": "./data.csv", + "./utils.py": "./lib/utils.py" + }, + "js_modules": { + "main": { + "https://cdn.jsdelivr.net/npm/chart.js": "chartjs" + } + } +} +``` + +```html title="index.html - Using the configuration." + + + + + My PyScript App + + + + +

Data Analysis

+
+ + + +``` + +```python title="main.py - Using the configured resources." +import numpy as np +from pyscript import display + + +# Use installed packages. +data = np.array([1, 2, 3, 4, 5]) +display(f"Mean: {data.mean()}") + +# Read configured files. +with open("data.csv") as f: + display(f"Data: {f.read()[:100]}") + +# Use JavaScript modules. +from pyscript.js_modules import chartjs +display("Chart.js loaded!") +``` + +## What's next + +Now that you understand configuration, explore these related topics: + +**[DOM Interaction](dom.md)** - Learn how to create and manipulate +elements for more control than `display()` provides. + +**[Events](events.md)** - Make your displayed content interactive by +handling user events. + +**[Architecture guide](architecture.md)** - provides technical details about +how PyScript implements workers using PolyScript and Coincident if you're +interested in the underlying mechanisms. + +**[Workers](workers.md)** - Configure Python code to run in background +threads with the same configuration options. + +**[Filesystem](filesystem.md)** - Learn more about the virtual +filesystem and how the `files` option works. + +**[FFI](ffi.md)** - Understand how JavaScript modules integrate with +Python through the foreign function interface. \ No newline at end of file diff --git a/docs/user-guide/display.md b/docs/user-guide/display.md new file mode 100644 index 0000000..d609ddf --- /dev/null +++ b/docs/user-guide/display.md @@ -0,0 +1,351 @@ +# Display + +The `display()` function is how to show Python objects, text, HTML, +images, and rich content in your web page. It introspects objects to +determine the best way to render them, supporting everything from +simple strings to matplotlib plots. + +Heavily inspired by [IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html). + +Think of `display()` as Python's `print()` for the web - but with +superpowers. + +## Basic usage + +The simplest use is displaying text: + +```python +from pyscript import display + + +display("Hello, World!") +display("Multiple", "values", "at", "once") +``` + +By default, `display()` outputs to the current script's location in the +page. Each call appends content unless you specify otherwise. + +### Display targets + +Control where content appears using the `target` parameter: + +```python +from pyscript import display + + +# Display in a specific element by ID. +display("Hello", target="output-div") + +# The '#' prefix is optional (and ignored if present). +display("Hello", target="#output-div") +``` + +### Replacing vs. appending + +By default, `display()` appends content. Use `append=False` to replace +existing content: + +```python +from pyscript import display + + +# Replace any existing content. +display("New content", target="output-div", append=False) + +# Append to existing content (default behaviour). +display("More content", target="output-div", append=True) +``` + +## Displaying different types + +### Strings + +Plain strings are automatically HTML-escaped for safety: + +```python +from pyscript import display + + +# This is safe - HTML tags are escaped. +display("") +# Displays: <script>alert('XSS')</script> +``` + +### HTML content + +To display unescaped HTML, wrap it in the `HTML` class: + +```python +from pyscript import HTML, display + + +# Render actual HTML. +display(HTML("

Hello, World!

")) +display(HTML("

This is bold text.

")) +``` + +!!! warning + + Only use `HTML()` with content you trust. Never use it with + user-provided data as it can create security vulnerabilities. + +### Python objects + +Most Python objects display using their `__repr__()` method: + +```python +from pyscript import display + + +# Numbers. +display(42) +display(3.14159) + +# Lists and dictionaries. +display([1, 2, 3, 4, 5]) +display({"name": "Alice", "age": 30}) + +# Custom objects. +class Person: + def __init__(self, name): + self.name = name + + def __repr__(self): + return f"Person(name='{self.name}')" + +display(Person("Bob")) +``` + +### Images + +Display images from various sources: + +```python +from pyscript import display + + +# From a URL (using HTML). +from pyscript import HTML + +display(HTML('')) + +# From matplotlib (if you have it configured). +import matplotlib.pyplot as plt + +fig, ax = plt.subplots() +ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) +display(fig) +``` + +PyScript automatically detects matplotlib figures and renders them as +PNG images. + +## Rich display system + +As has already been mentioned, PyScript's display system is inspired by +[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html). +Objects can implement special methods to control how they're rendered. + +### Representation methods + +Objects are checked for these methods in order of preference: + +1. `_repr_mimebundle_()` - Returns multiple format options. +2. `_repr_png_()` - Returns PNG image data. +3. `_repr_jpeg_()` - Returns JPEG image data. +4. `_repr_svg_()` - Returns SVG graphics. +5. `_repr_html_()` - Returns HTML content. +6. `_repr_json_()` - Returns JSON data. +7. `__repr__()` - Returns plain text representation. + +### Custom display for your objects + +Create objects that display beautifully: + +```python +from pyscript import display + + +class ColourSwatch: + """ + A colour swatch that displays as a coloured box. + """ + def __init__(self, colour, name): + self.colour = colour + self.name = name + + def _repr_html_(self): + return f""" +
+
+
+
+ {self.name} +
+
+ """ + + +# These display as visual colour swatches. +display(ColourSwatch("#FF0000", "Red")) +display(ColourSwatch("#00FF00", "Green")) +display(ColourSwatch("#0000FF", "Blue")) +``` + +### Multiple format support + +Provide multiple representations for maximum compatibility: + +```python +from pyscript import display + + +class DataTable: + """ + A table that can display as HTML or plain text. + """ + def __init__(self, data): + self.data = data + + def _repr_mimebundle_(self): + """ + Return multiple formats. + """ + html = "" + for row in self.data: + html += "" + for cell in row: + html += f"" + html += "" + html += "
{cell}
" + + text = "\n".join(["\t".join(str(c) for c in row) + for row in self.data]) + + return { + "text/html": html, + "text/plain": text + } + + +table = DataTable([ + ["Name", "Age", "City"], + ["Alice", 30, "London"], + ["Bob", 25, "Paris"] +]) + +display(table) +``` + +## Working with scripts + +### Script tag output + +When PyScript runs a ` +
+``` + +### Targeting specific elements + +Use the `target` attribute on your script tag to send output elsewhere: + +```html +
+ + +``` + +## Example: Data visualisation dashboard + +Here's a complete example showing various display capabilities: + + + +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/display-demo). + +This example demonstrates: + +- Displaying different data types. +- Creating custom display representations. +- Building UIs with `HTML()` (but prefer to use `pyscript.web` for this). +- Updating content dynamically. +- Using display targets effectively. + +## Best practices + +### Security + +**Never use `HTML()` with untrusted content:** + +```python +# DANGEROUS - don't do this! +user_input = "" +display(HTML(user_input)) + +# SAFE - plain strings are escaped automatically. +display(user_input) +``` + +### Performance + +For frequent or rapid updates: + +- Use `append=False` to replace content rather than accumulating it. +- For interactive UIs, use `pyscript.web` to manipulate elements directly + rather than repeatedly calling `display()`. +- When you need fine control over the DOM, `display()` **is for output, + not UI building**. + +### Organisation + +Keep display logic close to your data: + +```python +class Report: + def __init__(self, data): + self.data = data + + def _repr_html_(self): + """ + Encapsulate display logic in the class. + """ + return f"
{self.data}
" + + +# Clean usage. +report = Report({"sales": 1000}) +display(report) +``` + +## What's next + +Now that you understand how to display content, explore these related +topics: + +**[Configuration](configuration.md)** - Configure how your scripts load +and when event handlers are attached. + +**[DOM Interaction](dom.md)** - Learn how to create and manipulate +elements for more control than `display()` provides. + +**[Events](events.md)** - Make your displayed content interactive by +handling user events. + +**[Workers](workers.md)** - Display content from background threads +(requires explicit `target` parameter). \ No newline at end of file diff --git a/docs/user-guide/dom.md b/docs/user-guide/dom.md index 544b9ae..49507ad 100644 --- a/docs/user-guide/dom.md +++ b/docs/user-guide/dom.md @@ -2,452 +2,554 @@ The DOM ([document object model](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model)) -is a tree like data structure representing the web page displayed by the -browser. PyScript interacts with the DOM to change the user interface and react -to things happening in the browser. +is a tree-like data structure representing the web page displayed by +the browser. PyScript interacts with the DOM to change the user +interface and react to things happening in the browser. -There are currently two ways to interact with the DOM: +PyScript provides two ways to work with the DOM: -1. Through the [foreign function interface](#ffi) (FFI) to interact with objects found - in the browser's `globalThis` or `document` objects. -2. Through the [`pyscript.web` module](#pyscriptweb) that acts as a Pythonic wrapper around - the FFI and comes as standard with PyScript. +1. **The `pyscript.web` module** - A Pythonic interface that feels + natural to Python developers. This is the recommended approach for + most tasks. +2. **The FFI (foreign function interface)** - Direct access to + JavaScript APIs for advanced use cases or when you need complete + control. -## FFI +Both approaches are powerful and can accomplish the same goals. We'll +explore each one, starting with the Pythonic `pyscript.web` module. -The foreign function interface (FFI) gives Python access to all the -[standard web capabilities and features](https://developer.mozilla.org/en-US/docs/Web), -such as the browser's built-in -[web APIs](https://developer.mozilla.org/en-US/docs/Web/API). +!!! tip -This is available via the `pyscript.window` module which is a proxy for -the main thread's `globalThis` object, or `pyscript.document` which is a proxy -for the website's `document` object in JavaScript: + **New to PyScript?** Start with `pyscript.web`. It's designed to + feel natural if you know Python, and it handles many common tasks + more elegantly than direct JavaScript API calls. + + The FFI becomes useful when you need to integrate specific + JavaScript libraries or when you're already familiar with web + development in JavaScript. -```Python title="Accessing the window and document objects in Python" -from pyscript import window, document +## Quick start: pyscript.web +The `pyscript.web` module provides an idiomatic Python interface to the +DOM. It wraps the FFI in a way that feels natural to Python developers, +with familiar patterns like dictionary-style access, set-like class +management, and Pythonic method names. -my_element = document.querySelector("#my-id") -my_element.innerText = window.location.hostname -``` +### Finding elements -The FFI creates _proxy objects_ in Python linked to _actual objects_ in -JavaScript. +The `page` object represents your web page. Use it to find elements by +their ID or with CSS selectors: -The proxy objects in your Python code look and behave like Python -objects but have related JavaScript objects associated with them. It means the -API defined in JavaScript remains the same in Python, so any -[browser based JavaScript APIs](https://developer.mozilla.org/en-US/docs/Web/API) -or third party JavaScript libraries that expose objects in the web page's -`globalThis`, will have exactly the same API in Python as in JavaScript. +```python +from pyscript import web -The FFI automatically transforms Python and JavaScript objects into the -equivalent in the other language. For example, Python's boolean `True` and -`False` will become JavaScript's `true` and `false`, while a JavaScript array -of strings and integers, `["hello", 1, 2, 3]` becomes a Python list of the -equivalent values: `["hello", 1, 2, 3]`. -!!! info +# Get an element by ID (returns single Element or None). +header = web.page["header-id"] +header = web.page["#header-id"] # The "#" prefix is optional. - Instantiating classes into objects is an interesting special case that the - FFI expects you to handle. +# Find by CSS selector (returns an ElementCollection). +divs = web.page.find("div") +buttons = web.page.find(".button-class") +items = web.page.find("#list .item") - **If you wish to instantiate a JavaScript class in your Python - code, you need to call the class's `new` method:** +# Access page structure. +web.page.body.append(some_element) +web.page.title = "New Page Title" +``` - ```python - from pyscript import window +CSS selectors work exactly like they do in CSS or JavaScript's +[`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). +If you can select it in CSS, you can find it with `pyscript.web`. +### Creating elements - my_obj = window.MyJavaScriptClass.new("some value") +Create new HTML elements using simple Python classes. Element +names correspond to HTML tags, and are lower-case to match web +conventions. Compose elements together in a +[declarative style](https://en.wikipedia.org/wiki/Declarative_programming): - ``` +```python +from pyscript import web + + +# Create simple elements. +div = web.div("Hello, World!") +paragraph = web.p("Some text", id="my-para", classes=["intro"]) + +# Compose elements together. +container = web.div( + web.h1("My Task List"), + web.p("Keep track of your work"), + web.ul( + web.li("First task"), + web.li("Second task"), + web.li("Third task") + ), + id="task-container", + classes=["panel", "primary"] +) + +# Add to the page. +web.page.body.append(container) +``` - The underlying reason for this is simply JavaScript and Python do - instantiation very differently. By explicitly calling the JavaScript - class's `new` method PyScript both signals and honours this difference. +The first (unnamed) arguments to an element become its children. Named +arguments like `id`, `classes`, and `style` set HTML attributes. - More technical information about instantiating JavaScript classes can be - [found in the FAQ](../../faq/#javascript-classnew) +You can also create elements +[imperatively](https://en.wikipedia.org/wiki/Imperative_programming): -Should you require lower level API access to FFI features, you can find such -builtin functions under the `pyscript.ffi` namespace in both Pyodide and -MicroPython. The available functions are described in our section on the -[builtin API](../../api). +```python +from pyscript import web -Advanced users may wish to explore the -[technical details of the FFI](../ffi). -## `pyscript.web` +# Create an empty div. +my_div = web.div(id="my-container") -!!! warning +# Add content and styling. +my_div.innerHTML = "

Hello!

" +my_div.classes.add("active") +my_div.style["background-color"] = "lightblue" - The `pyscript.web` module is currently a work in progress. +# Create a paragraph and add it. +my_p = web.p("This is a paragraph.") +my_div.append(my_p) +``` - We welcome feedback and suggestions. +### Modifying content and attributes -The `pyscript.web` module is an idiomatically Pythonic API for interacting with -the DOM. It wraps the FFI in a way that is more familiar to Python developers -and works natively with the Python language. Technical documentation for this -module can be found in [the API](../../api/#pyscriptweb) section. +Once you have an element, you can modify its content and attributes +using idiomatic Python: -There are three core concepts to remember: +```python +from pyscript import web -* Find elements on the page via - [CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors). - The `find` API uses exactly the [same queries](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Locating_DOM_elements_using_selectors) - as those used by native browser methods like `qurerySelector` or - `querySelectorAll`. -* Use classes in the `pyscript.web` namespace to create and organise - new elements on the web page. -* Collections of elements allow you to access and change attributes en-mass. - Such collections are returned from `find` queries and are also used for the - [children](https://developer.mozilla.org/en-US/docs/Web/API/Element/children) - of an element. -You have several options for accessing the content of the page, and these are -all found in the `pyscript.web.page` object. The `html`, `head` and `body` -attributes reference the page's top-level html, head and body. As a convenience -the `page`'s `title` attribute can be used to get and set the web page's title -(usually shown in the browser's tab). The `append` method is a shortcut for -adding content to the page's `body`. Whereas, the `find` method is used to -return collections of elements matching a CSS query. You may also shortcut -`find` via a CSS query in square brackets. Finally, all elements have a `find` -method that searches within their children for elements matching your CSS -query. +element = web.page["my-element"] -```python -from pyscript.web import page +# Update content. +element.innerHTML = "Bold text" +element.textContent = "Plain text" +# Update attributes. +element.id = "new-id" +element.title = "Tooltip text" -# Print all the child elements of the document's head. -print(page.head.children) -# Find all the paragraphs in the DOM. -paragraphs = page.find("p") -# Or use square brackets. -paragraphs = page["p"] +# Bulk update with convenience method. +element.update( + classes=["active", "highlighted"], + style={"color": "red", "font-size": "16px"}, + title="Updated tooltip" +) ``` -The object returned from a query, or used as a reference to an element's -children is iterable: +### Working with classes and styles + +Element classes behave like a Python `set`, and styles behave like a +Python `dict`: ```python -from pyscript.web import page +from pyscript import web -# Get all the paragraphs in the DOM. -paragraphs = page["p"] +element = web.page["my-button"] -# Print the inner html of each paragraph. -for p in paragraphs: - print(p.html) -``` +# Classes work like sets. +element.classes.add("active") +element.classes.add("highlighted") +element.classes.remove("hidden") +element.classes.discard("maybe-not-there") # No error if missing. -Alternatively, it is also indexable / sliceable: +# Check membership. +if "active" in element.classes: + print("Element is active") -```python -from pyscript.web import page +# Clear all classes. +element.classes.clear() +# Styles work like dictionaries. +element.style["color"] = "red" +element.style["background-color"] = "#f0f0f0" +element.style["font-size"] = "16px" -# Get an ElementCollection of all the paragraphs in the DOM -paragraphs = page["p"] +# Remove a style. +del element.style["margin"] -# Only the final two paragraphs. -for p in paragraphs[-2:]: - print(p.html) +# Check if style is set. +if "color" in element.style: + print(f"Colour is {element.style['color']}") ``` -You have access to all the standard attributes related to HTML elements (for -example, the `innerHTML` or `value`), along with a couple of convenient ways -to interact with classes and CSS styles: +### Working with collections + +When you find multiple elements, you get an `ElementCollection` that +you can iterate over, slice, or update in bulk: -* `classes` - the list of classes associated with the elements. -* `style` - a dictionary like object for interacting with CSS style rules. +```python +from pyscript import web -For example, to continue the example above, `paragraphs.innerHTML` will return -a list of all the values of the `innerHTML` attribute on each contained -element. Alternatively, set an attribute for all elements contained in the -collection like this: `paragraphs.style["background-color"] = "blue"`. -It's possible to create new elements to add to the page: +# Find multiple elements (returns an ElementCollection). +items = web.page.find(".list-item") -```python -from pyscript.web import page, div, select, option, button, span, br - - -page.append( - div( - div("Hello!", classes="a-css-class", id="hello"), - select( - option("apple", value=1), - option("pear", value=2), - option("orange", value=3), - ), - div( - button(span("Hello! "), span("World!"), id="my-button"), - br(), - button("Click me!"), - classes=["css-class1", "css-class2"], - style={"background-color": "red"} - ), - div( - children=[ - button( - children=[ - span("Hello! "), - span("Again!") - ], - id="another-button" - ), - br(), - button("b"), - ], - classes=["css-class1", "css-class2"] - ) - ) +# Iterate over collection. +for item in items: + item.innerHTML = "Updated" + item.classes.add("processed") + +# Bulk update all elements. +items.update_all( + innerHTML="New content", + classes=["updated-item"] ) + +# Index and slice collections. +first = items[0] +last = items[-1] +subset = items[1:3] + +# Get an element by ID within the collection. +special = items["special-id"] + +# Find descendants within the collection. +subitems = items.find(".sub-item") ``` -This example demonstrates a declaritive way to add elements to the body of the -page. Notice how the first (unnamed) arguments to an element are its children. -The named arguments (such as `id`, `classes` and `style`) refer to attributes -of the underlying HTML element. If you'd rather be explicit about the children -of an element, you can always pass in a list of such elements as the named -`children` argument (you see this in the final `div` in the example above). +### Managing select elements -Of course, you can achieve similar results in an imperative style of -programming: +The `select` element contains a list of `option` instances from which you +select. When rendered on a web page, it looks like this: + + + + +The `options` property of `select` elements provides convenient methods +for managing such options: ```python -from pyscript.web import page, div, p +from pyscript import web -my_div = div() -my_div.style["background-color"] = "red" -my_div.classes.add("a-css-class") +# Get an existing select element. +select = web.page["my-select"] -my_p = p() -my_p.content = "This is a paragraph." +# Add options. +select.options.add(value="1", html="Option 1") +select.options.add(value="2", html="Option 2", selected=True) -my_div.append(my_p) +# Get the selected option. +selected = select.options.selected +print(f"Selected: {selected.value}") + +# Iterate over options. +for option in select.options: + print(f"{option.value}: {option.innerHTML}") -# etc... +# Clear all options. +select.options.clear() + +# Remove specific option by index. +select.options.remove(0) ``` -It's also important to note that the `pyscript.when` decorator understands -element references from `pyscript.web`: +This also works for `datalist` and `optgroup` elements, that also require +lists of options to function. + +### Event handling with pyscript.web + +The `@when` decorator works seamlessly with `pyscript.web` elements: ```python -from pyscript import when -from pyscript.web import page +from pyscript import when, web -btn = page["#my-button"] +# Create a button. +button = web.button("Click me", id="my-button") +web.page.body.append(button) +# Attach event handler with decorator. +@when("click", button) +def handle_click(event): + print("Button clicked!") -@when("click", btn) -def my_button_click_handler(event): - print("The button has been clicked!") +# Or attach during creation. +def another_handler(event): + print("Another handler") + +button2 = web.button("Click too", on_click=another_handler) ``` -Should you wish direct access to the proxy object representing the underlying -HTML element, each Python element has a `_dom_element` property for this -purpose. +Learn more about event handling in the [events guide](events.md). -Once again, the technical details of these classes are described in the -[built-in API documentation](../../api/#pyscriptweb). +### Direct DOM access -## Working with JavaScript +When needed, you can access the underlying DOM element directly: -There are three ways in which JavaScript can get into a web page. +```python +from pyscript import web -1. As a global reference attached to the `window` object in the web page - because the code was referenced as the source of a `script` tag in your HTML - (the very old school way to do this). -2. Using the [Universal Module Definition](https://github.com/umdjs/umd) (UMD), - an out-of-date and non-standard way to create JavaScript modules. -3. As a standard - [JavaScript Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) - which is the modern, standards compliant way to define and use a JavaScript - module. If possible, this is the way you should do things. -Sadly, this being the messy world of the web, methods 1 and 2 are still quite -common, and so you need to know about them so you're able to discern and work -around them. There's nothing WE can do about this situation, but we can -suggest "best practice" ways to work around each situation. +element = web.page["my-element"] -Remember, as mentioned -[elsewhere in our documentation](../configuration/#javascript-modules), -the standard way to get JavaScript modules into your PyScript Python context is -to link a _source_ standard JavaScript module to a _destination_ name: +# Most DOM methods are accessible directly. +element.scrollIntoView() +element.focus() +element.blur() -```toml title="Reference a JavaScript module in the configuration." -[js_modules.main] -"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js" = "leaflet" +# Convenience method for scrolling. +element.show_me() # Calls scrollIntoView(). + +# Access the raw underlying DOM element for special cases. +dom_element = element._dom_element ``` -Then, reference the module via the destination name in your Python code, by -importing it from the `pyscript.js_modules` namespace: +!!! info -```python title="Import the JavaScript module into Python" -from pyscript.js_modules import leaflet as L + For complete API documentation of `pyscript.web`, including all + available element types and methods, see the + [API reference](../../api/web). -map = L.map("map") +## Example: Task board with pyscript.web -# etc.... -``` +Let's look at a complete example that demonstrates these concepts. This +task board application lets users add tasks, mark them complete, filter +by priority, and delete tasks: -We'll deal with each of the potential JavaScript related situations in turn: + -### JavaScript as a global reference +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/task-board-web). -In this situation, you have some JavaScript code that just globally defines -"stuff" in the context of your web page via a `script` tag. Your HTML will -contain something like this: +Notice how the code uses Pythonic patterns throughout: dictionary-style +access for elements, set operations for classes, and familiar Python +syntax for creating and modifying elements. -```html title="JavaScript as a global reference" - - - +## The FFI: JavaScript interoperability - - -``` +The foreign function interface (FFI) gives Python direct access to all +the [standard web capabilities and features](https://developer.mozilla.org/en-US/docs/Web), +including the browser's built-in +[web APIs](https://developer.mozilla.org/en-US/docs/Web/API). -When you find yourself in this situation, simply use the `window` object in -your Python code (found in the `pyscript` namespace) to interact with the -resulting JavaScript objects: +This is available via the `pyscript.window` and `pyscript.document` +objects, which are proxies for JavaScript's `globalThis` and `document` +objects: -```python title="Python interaction with the JavaScript global reference" +```python from pyscript import window, document -# The window object is the global context of your web page. -html = window.html +# Access browser APIs. +hostname = window.location.hostname +current_url = window.location.href + +# Find and manipulate DOM elements. +my_element = document.querySelector("#my-id") +my_element.innerText = "Hello from Python!" -# Just use the object "as usual"... -# e.g. show escaped HTML in the body: <> -document.body.append(html.escape("<>")) +# Query all matching elements. +paragraphs = document.querySelectorAll("p") +for p in paragraphs: + p.style.color = "blue" ``` -You can find an example of this technique here: +### Proxy objects -[https://pyscript.com/@agiammarchi/floral-glade/v1](https://pyscript.com/@agiammarchi/floral-glade/v1) +The FFI creates _proxy objects_ in Python that are linked to _actual +objects_ in JavaScript. These proxy objects look and behave like Python +objects but have related JavaScript objects associated with them +"in the background" and automatically managed for you by the FFI. -### JavaScript as a non-standard UMD module +This means the API defined in JavaScript remains the same in Python, so +any [browser-based JavaScript APIs](https://developer.mozilla.org/en-US/docs/Web/API) +or third-party JavaScript libraries that expose objects in the web +page's `globalThis` will have exactly the same API in Python as in +JavaScript. -Sadly, these sorts of non-standard JavaScript modules are still quite -prevalent. But the good news is there are strategies you can use to help you -get them to work properly. +### Type conversions -The non-standard UMD approach tries to check for `export` and `module` fields -in the JavaScript module and, if it doesn’t find them, falls back to treating -the module in the same way as a global reference described above. +The FFI automatically transforms Python and JavaScript objects into +their equivalent in the other language: -If you find you have a UMD JavaScript module, there are services online to -automagically convert it to the modern and standards compliant way to d -o JavaScript modules. A common (and usually reliable) service is provided by -[https://esm.run/your-module-name](https://esm.run/your-module-name), a -service that provides an out of the box way to consume the module in the -correct and standard manner: +| Python | JavaScript | +|--------|------------| +| `True`, `False` | `true`, `false` | +| `None` | `null` or `undefined` | +| `int`, `float` | `number` | +| `str` | `string` | +| `list` | `Array` | +| `dict` | `Object` | -```html title="Use esm.run to automatically convert a non-standard UMD module" - - +For example, a JavaScript array `["hello", 1, 2, 3]` becomes a Python +list `["hello", 1, 2, 3]`, and vice versa. + +### JavaScript class instantiation + +Instantiating JavaScript classes requires special handling because +Python and JavaScript do it differently: + +```python +from pyscript import window + + +# To instantiate a JavaScript class, call its .new() method. +my_obj = window.MyJavaScriptClass.new("some value") + +# This is equivalent to JavaScript: new MyJavaScriptClass("some value") ``` -If a similar test works for the module you want to use, use the esm.run CDN -service within the `py` or `mpy` configuration file as explained at the start -of this section on JavaScript (i.e. you'll use it via the `pyscript.js_modules` -namespace). +The `.new()` method is required because Python and JavaScript handle +class instantiation very differently. By explicitly calling `.new()`, +PyScript signals and honours this difference. -If this doesn't work, assume the module is not updated nor migrated to a state -that can be automatically translated by services like esm.run. You could try an -alternative (more modern) JavaScript module to achieve you ends or (if it -really must be this module), you can wrap it in a new JavaScript module that -conforms to the modern standards. +!!! info -The following four files demonstrate this approach: + For more technical details about JavaScript class instantiation, + see the [FAQ](../../faq/#javascript-classnew). -```html title="index.html - still grab the script so it appears as a global reference." - -... - - -... +### Lower-level FFI features + +Advanced users who need lower-level access to FFI features can use +functions in the `pyscript.ffi` namespace, available in both Pyodide +and MicroPython. These functions are documented in the +[API reference](../../api/ffi). + +For deep technical details about how the FFI works, see the +[FFI technical guide](ffi.md). + +## Example: Task board with FFI + +Here's the same task board application implemented using the FFI and +direct JavaScript APIs instead of `pyscript.web`: + + + +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/task-board-ffi). + +Compare this implementation with the `pyscript.web` version above. +Notice how the FFI version uses JavaScript method names like +`querySelector` and `createElement`, whilst the `pyscript.web` version +uses Pythonic patterns. + + +## Working with JavaScript libraries + +There are three ways JavaScript code typically appears in web pages. +Understanding these helps you integrate JavaScript libraries into your +PyScript applications. + +### Standard JavaScript modules (recommended) + +Modern JavaScript uses ES6 modules with `import` and `export` +statements. This is the best way to integrate JavaScript: + +```toml title="pyscript.toml - Configure JavaScript modules" +[js_modules.main] +"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js" = "leaflet" ``` -```js title="wrapper.js - this grabs the JavaScript functionality from the global context and wraps it (exports it) in the modern standards compliant manner." -// get all utilities needed from the global. -const { escape, unescape } = globalThis.html; +Then import the module in your Python code: -// export utilities like a standards compliant module would do. -export { escape, unescape }; +```python title="main.py - Use the JavaScript module" +from pyscript.js_modules import leaflet as L + + +map = L.map("map") +# Use the library... ``` -```toml title="pyscript.toml - configure your JS modules as before, but use your wrapper instead of the original module." -[js_modules.main] -# will simulate a standard JS module -"./wrapper.js" = "html_escaper" +That's it! PyScript handles loading the module and making it available +in Python. + +!!! tip + + This is the recommended approach. Most modern JavaScript libraries + provide ES6 module builds that work perfectly with this method. + +### JavaScript as a global reference (legacy) + +Older JavaScript code may add objects directly to the global `window` +object via a ` + + + + + ``` -```python title="main.py - just import the module as usual and make use of it." -from pyscript import document +Access these global objects via the `window` object in Python: -# import the module either via -from pyscript.js_modules import html_escaper -# or via -from pyscript.js_modules.html_escaper import escape, unescape +```python title="main.py - Access global JavaScript" +from pyscript import window, document + + +# The window object is the global context. +html = window.html -# show on body: <> -document.body.append(html.escape("<>")) +# Use the object normally. +escaped = html.escape("<>") +document.body.append(escaped) ``` -You can see this approach in action here: +### UMD modules (legacy) -[https://pyscript.com/@agiammarchi/floral-glade/v2](https://pyscript.com/@agiammarchi/floral-glade/v2) +The Universal Module Definition (UMD) is an outdated, non-standard way +to create JavaScript modules. If you encounter UMD modules, you have +two options: -### A standard JavaScript module +**Option 1: Use esm.run** - This service automatically converts UMD +modules to standard ES6 modules: -This is both the easiest and best way to import any standard JS module into -Python. +```toml title="pyscript.toml - Use esm.run to convert UMD" +[js_modules.main] +"https://esm.run/html-escaper" = "html_escaper" +``` -You don't need to reference the script in your HTML, just define how the source -JavaScript module maps into the `pyscript.js_modules` namespace in your -configuration file, as explained above. +**Option 2: Create a wrapper** - If esm.run doesn't work, wrap the +module yourself: -That's it! +```html title="index.html - Load the UMD module globally" + +``` -Here is an example project that uses this approach: +```js title="wrapper.js - Wrap it as a standard module" +// Get utilities from the global scope. +const { escape, unescape } = globalThis.html; -[https://pyscript.com/@agiammarchi/floral-glade/v3](https://pyscript.com/@agiammarchi/floral-glade/v3) +// Export as a standard module. +export { escape, unescape }; +``` +```toml title="pyscript.toml - Reference your wrapper" +[js_modules.main] +"./wrapper.js" = "html_escaper" +``` + +```python title="main.py - Use it normally" +from pyscript.js_modules import html_escaper -### My own JavaScript code -If you have your own JavaScript work, just remember to write it as a standard -JavaScript module. Put simply, ensure you `export` the things you need to. For -instance, in the following fragment of JavaScript, the two functions are -exported from the module: +escaped = html_escaper.escape("<>") +``` + +### Your own JavaScript code -```js title="code.js - containing two functions exported as capabilities of the module." +Write your own JavaScript as standard ES6 modules by using `export`: + +```js title="code.js - Your JavaScript module" /* -Some simple JavaScript functions for example purposes. +Simple JavaScript functions for example purposes. */ export function hello(name) { @@ -461,33 +563,74 @@ export function fibonacci(n) { } ``` -Next, just reference this module in the usual way in your TOML or JSON -configuration file: +Reference it in your configuration: -```TOML title="pyscript.toml - references the code.js module so it will appear as the code module in the pyscript.js_modules namespace." +```toml title="pyscript.toml" [js_modules.main] "code.js" = "code" ``` -In your HTML, reference your Python script with this configuration file: +Use it from Python: -```html title="Reference the expected configuration file." - -``` - -Finally, just use your JavaScript module’s exported functions inside PyScript: - -```python title="Just call your bespoke JavaScript code from Python." +```python title="main.py" from pyscript.js_modules import code -# Just use the JS code from Python "as usual". greeting = code.hello("Chris") print(greeting) + result = code.fibonacci(12) print(result) ``` -You can see this in action in the following example project: +For more details on configuring JavaScript modules, see the +[configuration guide](configuration.md/#javascript-modules). + +## When to use which approach + +Both `pyscript.web` and the FFI are powerful tools. Here's when to use +each: + +### Use pyscript.web when: + +- **You're comfortable with Python** - The API feels natural to Python + developers, with Pythonic naming and patterns. +- **You're building from scratch** - Creating new elements and + composing interfaces is elegant and concise. +- **You value readability** - The code is self-documenting and easy to + understand. +- **You're teaching or learning** - The interface is easier to + explain and understand to Python learners. + +### Use the FFI when: + +- **You're integrating JavaScript libraries** - Direct access to + JavaScript APIs means no translation layer. +- **You're porting existing JavaScript code** - The API is identical, + making translation straightforward. +- **You need a specific browser API** - Some browser features don't + have `pyscript` based wrappers (yet!). +- **You're already familiar with web development** - If you know + JavaScript, the FFI feels natural. + +!!! tip + + You can mix both approaches in the same application. Use + `pyscript.web` for most tasks and drop down to the FFI when needed. + They work together seamlessly. + +## What's next + +Now that you understand DOM manipulation, explore these related topics: + +**[Events](events.md)** - Learn how to respond to user actions and +browser events with the `@when` decorator and event handlers. + +**[Display](display.md)** - Discover how to show Python objects, images, +charts, and rich content on your page with the `display()` function. + +**[Configuration](configuration.md)** - Configure your Python +environment, specify packages, and customise PyScript's behaviour. -[https://pyscript.com/@ntoll/howto-javascript/latest](https://pyscript.com/@ntoll/howto-javascript/latest) +**[Workers](workers.md)** - Run Python code in background threads for +responsive applications. \ No newline at end of file diff --git a/docs/user-guide/editor.md b/docs/user-guide/editor.md index ba54f0e..0964c82 100644 --- a/docs/user-guide/editor.md +++ b/docs/user-guide/editor.md @@ -1,248 +1,265 @@ -# Python editor +# Python Editor -The PyEditor is a core plugin. +PyScript includes a built-in code editor for creating interactive Python +coding environments in web pages. Based on +[CodeMirror](https://codemirror.net/), the editor provides syntax +highlighting, a run button, and the ability to edit and execute code +directly in the browser. + +This guide explains how to use the Python editor for tutorials, +demonstrations, and interactive coding experiences. !!! warning - Work on the Python editor is in its early stages. We have made it available - in this version of PyScript to give the community an opportunity to play, - experiment and provide feedback. + The Python editor is under active development. Future versions may + include refinements and changes based on community feedback. The + core functionality described here is stable, but details may evolve. + +## Basic usage + +Create an editor by setting the script type to `py-editor` for Pyodide +or `mpy-editor` for MicroPython: - Future versions of PyScript will include a more refined, robust and perhaps - differently behaving version of the Python editor. +```html + +``` -If you specify the type of a ` + ``` -However, different editors can share the same interpreter if they share the -same `env` attribute value. +The first editor uses Pyodide, the second uses MicroPython. Each has +completely separate state. The variable `a` in the second editor doesn't +exist in the first. -```html title="Two editors sharing the same MicroPython environment." +## Shared environments + +Editors can share state by using the same interpreter and `env` attribute: + +```html + ``` -The outcome of these code fragments should look something like this: +Both editors share the `"shared"` environment. Variables defined in one +editor are accessible in the other. Run the first editor, then the +second, and you'll see the second editor can access the first editor's +variables. - +The interpreter type and environment name appear in the top right corner +of each editor, showing which environment it belongs to. -!!! info +## Setup editors - Notice that the interpreter type, and optional environment name is shown - at the top right above the Python editor. +Sometimes you need boilerplate code that runs automatically without +cluttering the visible editor. The `setup` attribute handles this: - Hovering over the Python editor reveals the "run" button. +```html + -### Stop evaluation + +``` -Sometimes, for whatever reason, the fragment of code in the editor will never -complete. Perhaps it's stuck in an infinite loop and you need to stop the -evaluation of your code so you can fix the problem and start it again. +Setup editors don't appear on the page but execute before any other +editors in the same environment. This is particularly useful for +educational contexts where you want students to focus on specific code +without seeing all the scaffolding. -When the code is running, hovering over the editor will reveal a stop button -(where the run button was found). Click on it, confirm you want to stop your -code, and then the code will stop and the editor will refresh so you can fix -your code. +## Stopping execution -It looks something like this: +Code running in an editor sometimes needs to be stopped - perhaps it's +stuck in an infinite loop or taking too long. Hover over a running +editor to reveal the stop button where the run button was. Click it, +confirm, and the code stops executing: -### Setup - -Sometimes you need to create a pre-baked Pythonic context for a shared -environment used by an editor. This need is especially helpful in educational -situations where boilerplate code can be run, with just the important salient -code available in the editor. +The editor refreshes, letting you fix the problem and run again. -To achieve this end use the `setup` attribute within a `script` tag. The -content of this editor will not be shown, but will bootstrap the referenced -environment automatically before any following editor within the same -environment is evaluated. +## Keyboard shortcuts -```html title="Bootstrapping an environment with `setup`" - +Run code without clicking the button using keyboard shortcuts. Press +`Ctrl+Enter` (or `Cmd+Enter` on Mac, or `Shift+Enter`) to execute the +code. This speeds up the edit-run-debug cycle. - -``` +## Programmatic access -Finally, the `target` attribute allows you to specify a node into which the -editor will be rendered: - -```html title="Specify a target for the Python editor." - -
-``` - -## Editor VS Terminal - -The editor and terminal are commonly used to embed interactive Python code into -a website. However, there are differences between the two plugins, of which you -should be aware. - -The main difference is that a `py-editor` or `mpy-editor` is an isolated -environment (from the rest of PyScript that may be running on the page) and -its code always runs in a web worker. We do this to prevent accidental blocking -of the main thread that would freeze your browser's user interface. - -Because an editor is isolated from regular *py* or *mpy* scripts, one should -not expect the same behavior regular *PyScript* elements follow, most notably: - - * The editor's user interface is based on - [CodeMirror](https://codemirror.net/) and not on XTerm.js - [as it is for the terminal](../terminal). - * Code is evaluated all at once and asynchronously when the *Run* button is - pressed (not each line at a time, as in the terminal). - * The editor has listeners for `Ctrl-Enter` or `Cmd-Enter`, and - `Shift-Enter` to shortcut the execution of all the code. These shortcuts - make no sense in the terminal as each line is evaluated separately. - * There is a clear separation between the code and any resulting output. - * You may not use blocking calls (like `input`) with the editor, whereas - these will work if running the terminal via a worker. - * It's an editor! So simple or complex programs can be fully written without - running the code until ready. In the terminal, code is evaluated one line - at a time as it is typed in. - * There is no special reference to the underlying editor instance, while - there is both `script.terminal` or `__terminal__` in the terminal. - -## Read / Write / Execute - -Sometimes you need to programatically read, write or execute code in an -editor. Once PyScript has started, every py-editor/mpy-editor script tag gets -a `code` accessor attached to it. +Access and control editors from Python code using the `code` property: ```python from pyscript import document -# Grab the editor script reference. -editor = document.querySelector('#editor') +# Get reference to an editor. +editor = document.querySelector('#my-editor') -# Output the live content of the editor. +# Read the current code. print(editor.code) -# Update the live content of the editor. +# Update the code. editor.code = """ a = 1 b = 2 print(a + b) """ -# Evaluate the live code in the editor. -# This could be any arbitrary code to evaluate in the editor's Python context. +# Execute code programmatically. editor.process(editor.code) ``` -## Configuration +This lets you build interfaces that modify or execute editor content +based on user actions elsewhere on the page. + +## Custom rendering location + +By default, editors render where their script tag appears. Use the +`target` attribute to render elsewhere: -Unlike ` + +
+``` -If a `setup` editor is present, that's the only PyEditor that needs a config. -Any subsequent related editor will reuse the config parsed and bootstrapped for -the `setup` editor. +The editor appears inside the target element rather than replacing the +script tag. This gives you control over page layout. -## Run via keyboard +## Configuration -Depending on your operating system, a combination of either `Ctrl-Enter`, -`Cmd-Enter` or `Shift-Enter` will execute the code in the editor (no need to -move the mouse to click the run button). +Editors require explicit configuration through the `config` attribute. +They don't use `` or `` tags: -## Override run +```html + +``` -Sometimes you just need to override the way the editor runs code. +If using setup editors, only the setup editor needs configuration. All +subsequent editors in the same environment share that configuration. -The editor's `handleEvent` can be overridden to achieve this: +## Overriding execution -```html title="Overriding execution via handleEvent." - ``` -This -[live example](https://agiammarchi.pyscriptapps.com/pyeditor-iot-example/latest/) -shows how the editor can be used to execute code via a USB serial connection to -a connected MicroPython microcontroller. +This technique enables scenarios like +[executing code on connected hardware](https://agiammarchi.pyscriptapps.com/pyeditor-iot-example/latest/) +via USB serial connections to microcontrollers. + +## Editor versus terminal + +The editor and terminal both provide interactive Python experiences but +serve different purposes and work differently. + +The editor isolates code in workers and evaluates everything when you +click run. It's designed for writing complete programs, editing freely, +and running when ready. The clear separation between code and output +makes it ideal for tutorials and demonstrations. + +The terminal evaluates code line by line as you type, like a traditional +REPL. It supports blocking operations like `input()` in workers and +provides an XTerm.js interface. The terminal feels more like a +traditional Python session. + +Use editors when building coding tutorials, creating interactive +demonstrations, or letting users write and execute complete programs. +Use terminals when providing a REPL experience, showing command-line +style interaction, or needing `input()` support. + +## Accessibility considerations + +The editor traps the `tab` key for code indentation rather than moving +focus. This matches standard code editor behaviour but has accessibility +implications. + +We follow +[CodeMirror's accessibility guidance](https://codemirror.net/examples/tab/): +press `Esc` before `Tab` to move focus to the next element. Otherwise, +`Tab` indents code. -## Tab behavior +This provides both standard coding behaviour and an escape hatch for +keyboard navigation. -We currently trap the `tab` key in a way that reflects what a regular code -editor would do: the code is simply indented, rather than focus moving to -another element. +## What's next -We are fully aware of the implications this might have around accessibility so -we followed -[this detailed advice from Codemirror's documentation](https://codemirror.net/examples/tab/) -We have an *escape hatch* to move focus outside the editor. Press `esc` before -`tab` to move focus to the next focusable element. Otherwise `tab` indents -code. +Now that you understand the Python editor, explore these related topics: +**[Terminal](terminal.md)** - Use the alternative REPL-style +interface for interactive Python sessions. -## Still missing +**[PyGame](pygame-ce.md)** - Use PyGame-CE with PyScript, covering the +differences from traditional PyGame development and techniques for making +games work well in the browser. -The PyEditor is currently under active development and refinement, so features -may change (depending on user feedback). For instance, there is currently no -way to stop or kill a web worker that has got into difficulty from the editor -(hint: refreshing the page will reset things). +**[Plugins](plugins.md)** - Understand the plugin system, lifecycle hooks, +and how to write plugins that integrate with PyScript. \ No newline at end of file diff --git a/docs/user-guide/events.md b/docs/user-guide/events.md new file mode 100644 index 0000000..39d9958 --- /dev/null +++ b/docs/user-guide/events.md @@ -0,0 +1,403 @@ +# Events + +Events are how your PyScript application responds to user actions: +clicks, key presses, form submissions, mouse movements, and more. +PyScript provides the `@when` decorator, a powerful and Pythonic way to +connect Python functions to browser events. + +This guide explores the `@when` decorator and the custom `Event` class +for creating your own event system within Python code. + +## The @when decorator + +The `@when` decorator connects Python functions to browser events. When +the specified event occurs on matching elements, your function is +called automatically. + +### Basic usage + +The simplest form takes an event type and a CSS selector: + +```python +from pyscript import when + + +@when("click", "#my-button") +def handle_click(event): + """ + Called whenever the element with id 'my-button' is clicked. + """ + print("Button was clicked!") +``` + +The decorator attaches the function to all elements matching the +selector. If multiple elements match, the function is called for each +one when its event fires. + +### Event types + +You can handle any browser event. Common ones include: + +**Mouse events**: `click`, `dblclick`, `mousedown`, `mouseup`, +`mousemove`, `mouseenter`, `mouseleave`, `mouseover`, `mouseout` + +**Keyboard events**: `keydown`, `keyup`, `keypress` + +**Form events**: `submit`, `change`, `input`, `focus`, `blur` + +**Document events**: `DOMContentLoaded`, `load`, `resize`, `scroll` + +See the [MDN Event Reference](https://developer.mozilla.org/en-US/docs/Web/Events) +for a complete list. + +### CSS selectors + +The selector can be any valid CSS selector: + +```python +from pyscript import when + + +# By ID. +@when("click", "#submit-button") +def handle_submit(event): + print("Submit button clicked") + + +# By class. +@when("click", ".delete-btn") +def handle_delete(event): + print("Delete button clicked") + + +# By attribute. +@when("click", "[data-action='save']") +def handle_save(event): + print("Save button clicked") + + +# Complex selectors. +@when("click", "nav .menu-item:not(.disabled)") +def handle_menu(event): + print("Menu item clicked") +``` + +### Working with Element objects + +You can also pass `pyscript.web` Element objects directly instead of +CSS selectors: + +```python +from pyscript import when, web + + +# Create an element. +button = web.button("Click me", id="my-button") +web.page.body.append(button) + +# Attach event handler using the Element object. +@when("click", button) +def handle_click(event): + print("Button clicked!") +``` + +This works with `ElementCollection` objects too: + +```python +from pyscript import when, web + + +# Find multiple elements. +buttons = web.page.find(".action-button") + +# Attach handler to all of them. +@when("click", buttons) +def handle_button_click(event): + print(f"Button clicked: {event.target.textContent}") +``` + +## JavaScript event objects + +When a browser event occurs, your function receives a JavaScript event object +containing information about what happened. Handling such events +[is optional](#functions-without-event-arguments). + +### Common properties + +```python +from pyscript import when + + +@when("click", ".item") +def handle_click(event): + # The element that triggered the event. + target = event.target + + # The element the listener is attached to. + current_target = event.currentTarget + + # Event type. + event_type = event.type # "click" + + # Mouse position. + x = event.clientX + y = event.clientY + + # Keyboard keys. + key = event.key + ctrl_pressed = event.ctrlKey + shift_pressed = event.shiftKey +``` + +### Preventing defaults + +Some events have default browser behaviours you might want to prevent: + +```python +from pyscript import when + + +@when("submit", "#my-form") +def handle_submit(event): + # Prevent the form from actually submitting. + event.preventDefault() + + # Handle the submission in Python instead. + print("Form submitted via Python!") + + +@when("click", "a.external-link") +def handle_link(event): + # Prevent navigation. + event.preventDefault() + + # Handle the link click custom logic. + print(f"Would navigate to: {event.target.href}") +``` + +### Stopping propagation + +Events "bubble" up through the DOM tree. Stop this with +`stopPropagation()`: + +```python +from pyscript import when + + +@when("click", ".outer") +def handle_outer(event): + print("Outer clicked") + + +@when("click", ".inner") +def handle_inner(event): + print("Inner clicked") + # Stop the event from reaching .outer. + event.stopPropagation() +``` + +## Custom Pythonic events + +The `Event` class lets you create custom events within your Python +code. This is useful for decoupling parts of your application or +creating your own event-driven architecture. + +### Creating and using custom events + +```python +from pyscript import Event, when + + +# Create a custom event. +data_loaded = Event() + +# Add a listener using @when. +@when(data_loaded) +def handle_data_loaded(result): + print(f"Data loaded: {result}") + +# Later, trigger the event. +data_loaded.trigger({"items": [1, 2, 3], "count": 3}) +``` + +### Multiple listeners + +Custom events can have multiple listeners: + +```python +from pyscript import Event, when + + +user_logged_in = Event() + + +@when(user_logged_in) +def update_ui(user_data): + print(f"Welcome, {user_data['name']}!") + + +@when(user_logged_in) +def load_preferences(user_data): + print(f"Loading preferences for {user_data['id']}") + + +@when(user_logged_in) +def track_analytics(user_data): + print("Recording login event") + + +# Trigger once, all listeners are called. +user_logged_in.trigger({"id": 123, "name": "Alice"}) +``` + +### Bridging DOM and custom events + +You can trigger custom events from DOM event handlers: + +```python +from pyscript import Event, when + + +# Custom event. +button_clicked = Event() + +# Trigger custom event from DOM event. +@when("click", "#my-button") +def handle_dom_click(event): + # Do some processing. + button_text = event.target.textContent + + # Trigger custom event with processed data. + button_clicked.trigger({"text": button_text, "timestamp": "now"}) + + +# Handle the custom event elsewhere. +@when(button_clicked) +def handle_custom_event(data): + print(f"Button '{data['text']}' was clicked at {data['timestamp']}") +``` + +This pattern decouples DOM handling from business logic. + +## Advanced patterns + +### Stacking decorators + +You can stack `@when` decorators to handle multiple events with one +function: + +```python +from pyscript import when + + +@when("mouseenter", ".highlight") +@when("focus", ".highlight") +def highlight_element(event): + event.target.classList.add("highlighted") + + +@when("mouseleave", ".highlight") +@when("blur", ".highlight") +def unhighlight_element(event): + event.target.classList.remove("highlighted") +``` + +### Async event handlers + +Event handlers can be async functions: + +```python +from pyscript import when +import asyncio + + +@when("click", "#fetch-data") +async def fetch_data(event): + print("Fetching data...") + + # Simulate async operation. + await asyncio.sleep(2) + + print("Data fetched!") +``` + +### Event options + +Pass options to control how events are handled: + +```python +from pyscript import when + + +# Fire only once. +@when("click", "#one-time-button", once=True) +def handle_once(event): + print("This runs only once!") + + +# Use capture phase instead of bubble phase. +@when("click", "#container", capture=True) +def handle_capture(event): + print("Captured during capture phase") + + +# Mark as passive (can't prevent default). +@when("scroll", window, passive=True) +def handle_scroll(event): + # Can't call event.preventDefault() here. + print("Scrolling...") +``` + +See [addEventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) +for details. + +### Functions without event arguments + +If your handler doesn't need the event object, you can omit it: + +```python +from pyscript import when + + +@when("click", "#simple-button") +def simple_handler(): + """ + No event parameter needed. + """ + print("Button clicked!") +``` + +PyScript detects whether your function accepts arguments and calls it +appropriately. + +## Example: Interactive colour picker + +Here's a complete example demonstrating various event handling +patterns: + + + +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/colour-picker). + +This example shows: + +- Handling multiple event types (`input`, `click`, `change`). +- Working with form inputs. +- Updating the UI based on events. +- Custom events for decoupling logic. + +## What's next + +Now that you understand event handling, explore these related topics: + +**[DOM Interaction](dom.md)** - Learn how to find and manipulate +elements that you're attaching events to. + +**[Display](display.md)** - Discover how to show Python objects, images, +charts, and rich content on your page with the `display()` function. + +**[Configuration](configuration.md)** - Configure how your scripts load +and when event handlers are attached. + +**[Workers](workers.md)** - Discover how to handle events in background +threads for responsive applications. \ No newline at end of file diff --git a/docs/user-guide/features.md b/docs/user-guide/features.md deleted file mode 100644 index cf0f2d3..0000000 --- a/docs/user-guide/features.md +++ /dev/null @@ -1,87 +0,0 @@ -# Features - -
-
All the web
-
-

Pyscript gives you full access to the DOM and all - the web - APIs implemented by your browser.

- -

Thanks to the foreign - function interface (FFI), Python just works with all the browser has to - offer, including any third party JavaScript libraries that may be included - in the page.

- -

The FFI is bi-directional ~ it also enables JavaScript to access the - power of Python.

- -
All of Python
-
-

PyScript brings you two Python interpreters:

-
    -
  1. Pyodide - the original standard - CPython interpreter you know and love, but compiled to WebAssembly. -
  2. -
  3. MicroPython - a lean and - efficient reimplementation of Python3 that includes a comprehensive - subset of the standard library, compiled to WebAssembly.
  4. -
-

Because it is just regular CPython, Pyodide puts Python's deep and - diverse ecosystem of libraries, frameworks - and modules at your disposal. No matter the area of computing endeavour, - there's probably a Python library to help. Got a favourite library in - Python? Now you can use it in the browser and share your work with just - a URL.

-

MicroPython, because of its small size (170k) and speed, is especially - suited to running on more constrained browsers, such as those on mobile - or tablet devices. It includes a powerful sub-set of the Python standard - library and efficiently exposes the expressiveness of Python to the - browser.

-

Both Python interpreters supported by PyScript implement the - same FFI to bridge the gap between the worlds of Python - and the browser.

-
- -
AI and Data science built in
-
Python is famous for its extraordinary usefulness in artificial - intelligence and data science. The Pyodide interpreter comes with many of - the libraries you need for this sort of work already baked in.
- -
Mobile friendly MicroPython
-
-

Thanks to MicroPython in PyScript, there is a compelling story for - Python on mobile.

- -

MicroPython is small and fast enough that your app will start quickly - on first load, and almost instantly (due to the cache) on subsequent - runs.

- -
Parallel execution
-
Thanks to a browser technology called - web workers - expensive and blocking computation can run somewhere other than the main - application thread controlling the user interface. When such work is done - on the main thread, the browser appears frozen; web workers ensure - expensive blocking computation happens elsewhere. - Think of workers as independent subprocesses in your web page.
- -
Rich and powerful plugins
-
-

PyScript has a small, efficient yet powerful core called - PolyScript. Most of - the functionality of PyScript is actually implemented through PolyScript's - plugin system.

- -

This approach ensures a clear separation of concerns: PolyScript - can focus on being small, efficient and powerful, whereas the PyScript - related plugins allow us to Pythonically build upon the solid foundations - of PolyScript.

- -

Because there is a plugin system, folks - independent of the PyScript core team have a way to create and - contribute to a rich ecosystem of plugins whose functionality reflects the - unique and diverse needs of PyScript's users.

-
-
- - diff --git a/docs/user-guide/ffi.md b/docs/user-guide/ffi.md index 1470e67..dee0bac 100644 --- a/docs/user-guide/ffi.md +++ b/docs/user-guide/ffi.md @@ -1,261 +1,259 @@ -# PyScript FFI - -The foreign function interface (FFI) gives Python access to JavaScript, and -JavaScript access to Python. As a result PyScript is able to access all the -standard APIs and capabilities provided by the browser. - -We provide a unified `pyscript.ffi` because -[Pyodide's FFI](https://pyodide.org/en/stable/usage/api/python-api/ffi.html) -is only partially implemented in MicroPython and there are some fundamental -differences. The `pyscript.ffi` namespace smooths out such differences into -a uniform and consistent API. - -Our `pyscript.ffi` offers the following utilities: - -* `ffi.to_js(reference)` converts a Python object into its JavaScript - counterpart. -* `ffi.create_proxy(def_or_lambda)` proxies a generic Python function into a - JavaScript one, without destroying its reference right away. -* `ffi.is_none(reference)` to check if a specific value is either `None` or `JsNull`. - -Should you require access to Pyodide or MicroPython's specific version of the -FFI you'll find them under the `pyodide.ffi` and `micropython.ffi` namespaces. -Please refer to the documentation for those projects for further information. - -## to_js - -In the -[Pyodide project](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.to_js), -this utility converts Python dictionaries into -[JavaScript `Map` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). -Such `Map` objects reflect the `obj.get(field)` semantics native to Python's -way of retrieving a value from a dictionary. - -Unfortunately, this default conversion breaks the vast majority of native and -third party JavaScript APIs. This is because the convention in idiomatic -JavaScript is to use an [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) -for such key/value data structures (not a `Map` instance). - -A common complaint has been the repeated need to call `to_js` with the long -winded argument `dict_converter=js.Object.fromEntries`. It turns out, most -people most of the time simply want to map a Python `dict` to a JavaScript -`object` (not a `Map`). - -Furthermore, in MicroPython the default Python `dict` conversion is to the -idiomatic and sensible JavaScript `object`, making the need to specify a -dictionary converter pointless. - -Therefore, if there is no reason to hold a Python reference in a JavaScript -context (which is 99% of the time, for common usage of PyScript) then use the -`pyscript.ffi.to_js` function, on both Pyodide and MicroPython, to always -convert a Python `dict` to a JavaScript `object`. - -```html title="to_js: pyodide.ffi VS pyscript.ffi" - - - - - -``` +# Foreign Function Interface + +The Foreign Function Interface (FFI) enables Python and JavaScript to +work together seamlessly. Python code can call JavaScript functions, +access browser APIs, and manipulate the DOM directly. JavaScript code +can call Python functions and access Python objects. This +interoperability is what makes PyScript possible. + +PyScript provides a unified FFI through `pyscript.ffi` that works +consistently across both Pyodide and MicroPython interpreters. This +guide explains how to use the FFI to bridge between Python and +JavaScript when necessary. + +## When to use the FFI + +The FFI is a low-level interface for situations where higher-level +abstractions don't suffice. Most of the time, you should prefer +`pyscript.web` for DOM manipulation, `pyscript.media` for device +access, and other purpose-built APIs. These modules use the FFI +internally whilst providing cleaner, more Pythonic interfaces. + +Use the FFI directly when you need to work with JavaScript libraries +that don't have PyScript wrappers, access browser APIs not yet covered +by PyScript modules, or pass Python functions as callbacks to JavaScript +code. -!!! Note +For DOM manipulation specifically, always prefer `pyscript.web` over +direct FFI usage. The FFI examples in this guide focus on situations +where `pyscript.web` doesn't apply. - It is still possible to specify a different `dict_converter` or use Pyodide - specific features while converting Python references by simply overriding - the explicit field for `dict_converter`. +## Converting Python to JavaScript - However, we cannot guarantee all fields and features provided by Pyodide - will work in the same way on MicroPython. +The `to_js()` function converts Python objects into their JavaScript +equivalents: + +```python +from pyscript.ffi import to_js + + +# Python dict becomes JavaScript object. +options = {"title": "Hello", "icon": "icon.png"} +js_options = to_js(options) + +# Python list becomes JavaScript array. +numbers = [1, 2, 3, 4, 5] +js_array = to_js(numbers) +``` -## create_proxy +This conversion is essential when calling JavaScript APIs that expect +JavaScript objects rather than Python objects. The function handles the +translation automatically, converting dictionaries to JavaScript objects +(not Maps), lists to arrays, and other common types appropriately. -In the -[Pyodide project](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.create_proxy), -this function ensures that a Python callable associated with an event listener, -won't be garbage collected immediately after the function is assigned to the -event. Therefore, in Pyodide, if you do not wrap your Python function, it is -immediately garbage collected after being associated with an event listener. +!!! info -This is so common a gotcha (see the FAQ for -[more on this](../../faq#borrowed-proxy)) that the Pyodide project have already -created many work-arounds to address this situation. For example, the -`create_once_callable`, `pyodide.ffi.wrappers.add_event_listener` and -`pyodide.ffi.set_timeout` are all methods whose goal is to automatically manage -the lifetime of the passed in Python callback. + PyScript's `to_js()` differs from Pyodide's version by defaulting + to converting Python dictionaries into JavaScript objects rather + than Maps. This matches what most JavaScript APIs expect and aligns + with MicroPython's behaviour, providing consistency across + interpreters. -Add to this situation methods connected to the JavaScript `Promise` object -(`.then` and `.catch` callbacks that are implicitly handled to guarantee no -leaks once executed) and things start to get confusing and overwhelming with -many ways to achieve a common end result. +## Creating function proxies -Ultimately, user feedback suggests folks simply want to do something like this, -as they write their Python code: +When passing Python functions to JavaScript, you must create a proxy to +prevent garbage collection: -```python title="Define a callback without create_proxy." -import js -from pyscript import window +```python +from pyscript.ffi import create_proxy +from pyscript import document -def callback(msg): +def handle_click(event): """ - A Python callable that logs a message. + Handle button clicks. """ - window.console.log(msg) + print("Button clicked!") -# Use the callback without having to explicitly create_proxy. -js.setTimeout(callback, 1000, 'success') +# Create a proxy for the JavaScript event listener. +button = document.getElementById("my-button") +button.addEventListener("click", create_proxy(handle_click)) ``` -Therefore, PyScript provides an experimental configuration flag called -`experimental_create_proxy = "auto"`. When set, you should never have to care -about these technical details nor use the `create_proxy` method and all the -JavaScript callback APIs should just work. +Without `create_proxy()`, the Python function would be garbage collected +immediately, causing the event listener to fail. The proxy maintains a +reference, keeping the function alive for JavaScript to call. -Under the hood, the flag is strictly coupled with the JavaScript garbage -collector that will eventually destroy all proxy objects created via the -[FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) -built into the browser. +!!! warning -This flag also won't affect MicroPython because it rarely needs a -`create_proxy` at all when Python functions are passed to JavaScript event -handlers. MicroPython automatically handles this situation. However, -there might still be rare and niche cases in MicroPython where such a -conversion might be needed. + When using `pyscript.web` with the `@when` decorator, proxies are + created automatically. You only need `create_proxy()` when working + directly with JavaScript APIs. -Hence, PyScript retains the `create_proxy` method, even though it does not -change much in the MicroPython world, although it might be still needed with -the Pyodide runtime is you don't use the `experimental_create_proxy = "auto"` -flag. +## Checking for null values -At a more fundamental level, MicroPython doesn't provide (yet) a way to -explicitly destroy a proxy reference, whereas Pyodide still expects to -explicitly invoke `proxy.destroy()` when the function is not needed. +JavaScript has both `null` and `undefined`. Python has `None`. The +`is_none()` function checks for both: + +```python +from pyscript.ffi import is_none +from pyscript import js -!!! warning - In MicroPython proxies might leak due to the lack of a `destroy()` method. - - Happily, proxies are usually created explicitly for event listeners or - other utilities that won't need to be destroyed in the future. So the lack - of a `destroy()` method in MicroPython is not a problem in this specific, - and most common, situation. - - Until we have a `destroy()` in MicroPython, we suggest testing the - `experimental_create_proxy` flag with Pyodide so both runtimes handle - possible leaks automatically. - -For completeness, the following examples illustrate the differences in -behaviour between Pyodide and MicroPython: - -```html title="A classic Pyodide gotcha VS MicroPython" - - - - - +value = js.document.getElementById("nonexistent") +if is_none(value): + print("Element not found") ``` -To address the difference in Pyodide's behaviour, we can use the experimental -flag: +This handles the mismatch between Python's single null-like value and +JavaScript's multiple null-like values, providing consistent behaviour +across interpreters. + +## Merging JavaScript objects + +The `assign()` function merges JavaScript objects, similar to +`Object.assign()` in JavaScript: + +```python +from pyscript.ffi import assign, to_js +from pyscript import js -```html title="experimental create_proxy" - - experimental_create_proxy = "auto" - - - +# Create a base object. +options = js.Object.new() + +# Merge in properties. +assign(options, {"width": 800}, {"height": 600}) ``` -Alternatively, `create_proxy` via the `pyscript.ffi` in both interpreters, but -only in Pyodide can we then destroy such proxy: - -```html title="pyscript.ffi.create_proxy" - - +This is useful when building configuration objects for JavaScript +libraries that expect objects built through mutation rather than created +whole. + +## Accessing JavaScript globals + +The `js` module provides access to JavaScript's global namespace: + +```python +from pyscript import js + + +# Call JavaScript functions. +js.console.log("Hello from Python!") + +# Access browser APIs. +js.alert("This is an alert") + +# Create JavaScript objects. +date = js.Date.new() +print(date.toISOString()) ``` -## is_none +Through `js`, you can access anything available in JavaScript's global +scope, including browser APIs, third-party libraries loaded via script +tags, and built-in JavaScript objects. -*Pyodide* version `0.28` onwards has introduced a new *nullish* value that -precisely represents JavaScript's `null` value. +## Example: Task board with direct DOM manipulation -Previously, both JavaScript `null` and `undefined` would have been converted -into Python's `None` but, alas, some APIs behave differently if a value is -`undefined` or explicitly `null`. +The task board example demonstrates FFI usage for direct DOM +manipulation, contrasting with the more Pythonic `pyscript.web` +approach: -For example, in *JSON*, `null` would survive serialization while `undefined` -would vanish. To preserve that distinction in *Python*, the conversion -between *JS* and *Python* now has a new `pyodide.ffi.jsnull` as explained in -the -[pyodide documentation](https://pyodide.org/en/stable/usage/type-conversions.html#javascript-to-python). + -In general, there should be no surprises. But, especially when dealing with the -*DOM* world, most utilities and methods return `null`. +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/task-board-ffi). -To simplify and smooth-out this distinction, we decided to introduce `is_null`, -as [demoed here](https://pyscript.com/@agiammarchi/pyscript-ffi-is-none/latest?files=main.py): +This example intentionally uses the FFI directly rather than +`pyscript.web`, showing how to work with the DOM at a lower level when +necessary. Compare this to +[the `pyscript.web` version](../example-apps/task-board-web/info.md) to see why the +higher-level API is Pythonically preferable. -```html title="pyscript.ffi.is_none" - - +# Create button with event handler. +delete_btn = document.createElement("button") +delete_btn.textContent = "Delete" +delete_btn.addEventListener("click", create_proxy(delete_handler)) + +# Assemble the DOM. +task_div.appendChild(delete_btn) +container.appendChild(task_div) ``` -Please note that in *MicroPython* the method works the same but, as we try to -reach feature-parity among runtimes, it is suggested to use `is_none(ref)` -even if, right now, there is no such distinction between `null` and -`undefined` in MicroPython. +This works, but `pyscript.web` would express the same logic more clearly, +Pythonically and with less boilerplate. + +## Understanding interpreter differences + +Pyodide and MicroPython implement JavaScript interop differently. +PyScript's FFI abstracts these differences, but understanding them helps +when debugging issues. + +Pyodide provides comprehensive FFI features including detailed type +conversion control, whilst MicroPython offers a simpler, more +straightforward implementation. PyScript's unified FFI provides a +consistent interface that works reliably on both, defaulting to +sensible behaviours that match common use cases. + +For interpreter-specific FFI features, access them through +`pyodide.ffi` or `micropython.ffi` directly. However, this breaks +cross-interpreter compatibility and should only be done when absolutely +necessary. + +## Worker context utilities + +When working with workers, the FFI provides additional utilities for +cross-thread communication. The `direct`, `gather`, and `query` +functions help manage objects and data across thread boundaries. These +are advanced features covered in detail in the +[workers API docs](../api/workers.md) and are primarily relevant when building +complex multi-threaded applications. + +## In summary + +Prefer higher-level APIs when they exist. Use `pyscript.web` for DOM +work, `pyscript.media` for devices, and other purpose-built modules +rather than reaching for the FFI directly. + +Create proxies for Python callbacks passed to JavaScript. Without +proxies, functions get garbage collected and event handlers fail. + +Convert Python objects to JavaScript when calling browser APIs. Most +JavaScript functions expect JavaScript objects, not Python objects, so +use `to_js()` when passing dictionaries or complex data structures. + +Handle null values correctly. JavaScript's `null` and `undefined` both +exist alongside Python's `None`, so use `is_none()` for reliable null +checking. + +## What's next + +Now that you understand the FFI, explore these related topics: + +**[Architecture guide](architecture.md)** - provides technical details about +how PyScript implements workers using PolyScript and Coincident if you're +interested in the underlying mechanisms. + +**[Workers](workers.md)** - Display content from background threads +(requires explicit `target` parameter). + +**[Filesystem](filesystem.md)** - Learn more about the virtual +filesystem and how the `files` option works. + +**[FFI](ffi.md)** - Understand how JavaScript modules integrate with +Python through the foreign function interface. + +**[Offline](offline.md)** - Use PyScript while not connected to the internet. \ No newline at end of file diff --git a/docs/user-guide/filesystem.md b/docs/user-guide/filesystem.md index b977a3f..232e566 100644 --- a/docs/user-guide/filesystem.md +++ b/docs/user-guide/filesystem.md @@ -1,176 +1,235 @@ -# PyScript and Filesystems +# Filesystem -As you know, the filesystem is where you store files. For Python to work there -needs to be a filesystem in which Python packages, modules and data for your -apps can be found. When you `import` a library, or when you `open` a file, it -is on the in-browser virtual filesystem that Python looks. +As you know, the filesystem is where you store files. For Python to work, there +needs to be a filesystem in which Python packages, modules, and data for your +applications can be found. When you `import` a library or `open()` a file, +Python looks in the filesystem. -However, things are not as they may seem. +However, things are not as they may seem in the browser environment. -This section clarifies what PyScript means by a filesystem, and the way in -which PyScript interacts with such a concept. +This guide clarifies what PyScript means by a filesystem, and the ways in which +PyScript interacts with such concepts. ## Two filesystems -PyScript interacts with two filesystems. +PyScript interacts with two distinct filesystems. -1. The browser, thanks to - [Emscripten](https://emscripten.org/docs/api_reference/Filesystem-API.html), - provides a virtual in-memory filesystem. **This has nothing to do with your - device's local filesystem**, but is contained within the browser based - sandbox used by PyScript. The [files](../configuration/#files) - configuration API defines what is found on this filesystem. -2. PyScript provides an easy to use API for accessing your device's local - filesystem. It requires permission from the user to mount a folder from the - local filesystem onto a directory in the browser's virtual filesystem. Think - of it as gate-keeping a bridge to the outside world of the device's local - filesystem. +The first is provided by the browser, thanks to +[Emscripten](https://emscripten.org/docs/api_reference/Filesystem-API.html). +This is a virtual in-memory filesystem that has nothing to do with your +device's local filesystem. It exists entirely within the browser based sandbox +used by PyScript. The [`files` configuration option](configuration.md#files) +defines what is found on this filesystem. + +The second filesystem is your device's actual local filesystem - the hard drive +on your laptop, mobile, or tablet. PyScript provides an API for accessing it, +but this requires explicit permission from the user. Think of it as +gate-keeping a bridge between the sandboxed world of the browser and the +outside world of your device's filesystem. Once mounted, *a folder from your +local filesystem appears at a directory in the browser's virtual filesystem*. !!! danger - Access to the device's local filesystem **is only available in Chromium - based browsers**. The maximum capacity for files shared in this way is - 4GB. + Access to the device's local filesystem is only available in recent Chromium + based browsers. The maximum capacity for files shared in this way is 4GB. - Firefox and Safari do not support this capability (yet), and so it is not + Firefox and Safari do not support this capability yet, so it is not available to PyScript running in these browsers. -## The in-browser filesystem +## The in-browser virtual filesystem The filesystem that both Pyodide and MicroPython use by default is the [in-browser virtual filesystem](https://emscripten.org/docs/api_reference/Filesystem-API.html). Opening files and importing modules takes place in relation to this sandboxed -environment, configured via the [files](../configuration/#files) entry in your -settings. +environment. You configure it via the [`files` entry in your +settings](configuration.md#files). + +Here's a simple example. First, configure a file to be fetched: -```toml title="Filesystem configuration via TOML." -[files] -"https://example.com/myfile.txt": "" +```json +{ + "files": { + "https://example.com/myfile.txt": "./myfile.txt" + } +} ``` -```python title="Just use the resulting file 'as usual'." -# Interacting with the virtual filesystem, "as usual". +Then use the resulting file as usual from Python: + +```python with open("myfile.txt", "r") as myfile: print(myfile.read()) ``` -Currently, each time you re-load the page, the filesystem is recreated afresh, -so any data stored by PyScript to this filesystem will be lost. +Currently, each time you reload the page, the filesystem is recreated afresh. +Any data stored by PyScript to this filesystem will be lost when you reload. In +the future, we may make it possible to configure the in-browser virtual +filesystem as persistent across reloads. !!! info - In the future, we may make it possible to configure the in-browser virtual - filesystem as persistent across re-loads. - -[This article](https://emscripten.org/docs/porting/files/file_systems_overview.html) -gives an excellent overview of the browser based virtual filesystem's -implementation and architecture. + The [Emscripten filesystem article](https://emscripten.org/docs/porting/files/file_systems_overview.html) + provides an excellent technical overview of the browser based virtual + filesystem's implementation and architecture. -The most important key concepts to remember are: - -* The PyScript filesystem is contained *within* the browser's sandbox. -* Each instance of a Python interpreter used by PyScript runs in a separate - sandbox, and so does NOT share virtual filesystems. -* All Python related filesytem operations work as expected with this - filesystem. -* The virtual filesystem is configured via the - [files](../configuration/#files) entry in your settings. -* The virtual filesystem is (currently) NOT persistent between page re-loads. -* Currently, the filesystem has a maximum capacity of 4GB of data (something - over which we have no control). +In summary, the PyScript filesystem is +contained within the browser's sandbox, and each instance of a Python +interpreter used by PyScript runs in a separate sandbox. They do not share +virtual filesystems. All Python related filesystem operations work as expected +with each isolated filesystem. The virtual filesystem is configured via the `files` +entry in your settings. Currently, the filesystem is not persistent between +page reloads, and has a maximum capacity of 4GB of data (a limitation we cannot +control). ## The device's local filesystem -**Access to the device's local filesystem currently only works on Chromium -based browsers**. +!!! warning + + Access to the device's local filesystem currently only works on recent + Chromium based browsers. -Your device (the laptop, mobile or tablet) that runs your browser has a -filesystem provided by a hard drive. Thanks to the -[`pyscript.fs` namespace in our API](../../api/#pyscriptfs), both MicroPython -and Pyodide (CPython) gain access to this filesystem should the user of -your code allow this to happen. +Your device that runs your browser has a filesystem provided by a hard drive. +Thanks to the `pyscript.fs` namespace, both MicroPython and Pyodide can gain +access to this filesystem, should the user of your code allow this to happen. -This is a [transient activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation) +This requires what's called a +[transient activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation) for the purposes of [user activation of gated features](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation). Put simply, before your code gains access to their local filesystem, an explicit agreement needs to be gathered from the user. Part of this process -involves asking the user to select a target directory on their local -filesystem, to which PyScript will be given access. +involves asking the user to select a target directory on their local filesystem +to which PyScript will be given access. The directory on their local filesystem, selected by the user, is then mounted -to a given directory inside the browser's virtual filesystem. In this way a -mapping is made between the sandboxed world of the browser, and the outside +to a given directory inside the browser's virtual filesystem. In this way, a +mapping is made between the sandboxed world of the browser and the outside world of the user's filesystem. Your code will then be able to perform all the usual filesystem related -operations provided by Python, within the mounted directory. However, **such -changes will NOT take effect on the local filesystem UNTIL your code -explicitly calls the `sync` function**. At this point, the state of the -in-browser virtual filesystem and the user's local filesystem are synchronised. +operations provided by Python within the mounted directory. However, such +changes will not take effect on the local filesystem until your code explicitly +calls the `sync()` function. At this point, the state of the in-browser virtual +filesystem and the user's local filesystem are synchronised. + +!!! warning + + Changes you make to mounted files are not automatically saved to the local + filesystem. You must call `fs.sync()` to persist them. The following code demonstrates the simplest use case: -```python title="The core operations of the pyscript.fs API" +```python +# Chromium only browsers! from pyscript import fs -# Ask once for permission to mount any local folder -# into the virtual filesystem handled by Pyodide/MicroPython. -# The folder "/local" refers to the directory on the virtual -# filesystem to which the user-selected directory will be + +# Ask for permission to mount any local folder into the virtual +# filesystem. The folder "/local" refers to the directory on the +# virtual filesystem to which the user-selected directory will be # mounted. await fs.mount("/local") -# ... DO FILE RELATED OPERATIONS HERE ... +# Do file related operations here. +with open("/local/data.txt", "w") as f: + f.write("Hello from PyScript!") -# If changes were made, ensure these are persisted to the local filesystem's -# folder. +# Ensure changes are persisted to the local filesystem's folder. await fs.sync("/local") -# If needed to free RAM or that specific path, sync and unmount +# If needed, sync and unmount to free RAM. await fs.unmount("/local") ``` It is possible to use multiple different local directories with the same mount point. This is important if your application provides some generic -functionality on data that might be in different local directories because -while the nature of the data might be similar, the subject is not. For -instance, you may have different models for a PyScript based LLM in different -directories, and may wish to switch between them at runtime using different -handlers (requiring their own transient action). In which case use -the following technique: - -```python title="Multiple local directories on the same mount point" -# Mount a local folder specifying a different handler. -# This requires a user explicit transient action (once). +functionality on data that might be in different local directories. For +instance, you may have different versions of data for a PyScript based application in +different directories, and may wish to switch between them at runtime. In this +case, use the following technique: + +```python +# Mount a local folder specifying a different handler. This requires a +# user explicit transient action (once). await fs.mount("/local", id="v1") -# ... operate on that folder ... + +# Operate on that folder. +with open("/local/model.dat", "r") as f: + data = f.read() await fs.unmount("/local") -# Mount a local folder specifying a different handler. -# This also requires a user explicit transient action (once). +# Mount a different local folder specifying a different handler. This +# also requires a user explicit transient action (once). await fs.mount("/local", id="v2") -# ... operate on that folder ... + +# Operate on that folder. +with open("/local/model.dat", "r") as f: + data = f.read() await fs.unmount("/local") -# Go back to the original handler or a previous one. -# No transient action required now. +# Go back to the original handler. No transient action required now. await fs.mount("/local", id="v1") -# ... operate again on that folder ... + +# Operate again on that folder. ``` -In addition to the mount `path` and handler `id`, the `fs.mount` function can -take two further arguments: +In addition to the mount `path` and handler `id`, the `fs.mount()` function +accepts two further arguments. The `mode` parameter (by default `"readwrite"`) +indicates the sort of activity available to the user. It can also be set to +`"read"` for read-only access to the local filesystem. This is part of the +[web standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#mode) +for directory selection. The `root` parameter (by default `""`) is 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). + +The `sync()` and `unmount()` functions only accept the mount `path` used in the +browser's local filesystem. -* `mode` (by default `"readwrite"`) indicates the sort of activity available to - the user. It can also be set to `read` for read-only access to the local - filesystem. This is a part of the - [web-standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#mode) - for directory selection. -* `root` - (by default, `""`) is 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). +!!! info -The `sync` and `unmount` functions only accept the mount `path` used in the -browser's local filesystem. + Mounting requires user activation. It must happen during or shortly after a + user interaction like a button click. This security requirement prevents sites + from accessing your filesystem without your knowledge. **Always attach your + `fs.mount()` call to a button click or similar user action**. + +## Example: Note-taking application + +Here's a simple note-taking application demonstrating local filesystem access: + +!!! warning + + If the "Select Folder" button appears unresponsive, it's because your + browser does not support local filesystem access. + + Remember, **this is a Chromium only feature** at this moment in time. + + + +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/note-taker). + +This application lets you select a folder on your computer and save notes into +it. The notes persist between page reloads because they're saved to your actual +filesystem, not the browser's temporary virtual filesystem. + +Click "Select Folder" and the browser will prompt you to choose a directory. +Once mounted, you can type your note and click "Save Note". The file will be +written to your chosen folder and will still be there after you close the +browser. + +## What's next + +Now that you understand PyScript's filesystems, explore these related topics +to deepen your knowledge. + +**[Workers](workers.md)** - Display content from background threads +(requires explicit `target` parameter). + +**[FFI](ffi.md)** - Understand how JavaScript modules integrate with +Python through the foreign function interface. + +**[Media](media.md)** - Capture photos and video with the camera or +record audio with your microphone. + +**[Offline](offline.md)** - Use PyScript while not connected to the internet. \ No newline at end of file diff --git a/docs/user-guide/first-steps.md b/docs/user-guide/first-steps.md deleted file mode 100644 index b1d7442..0000000 --- a/docs/user-guide/first-steps.md +++ /dev/null @@ -1,165 +0,0 @@ -# First steps - -It's simple: - -* tell your browser to use PyScript, then, -* tell PyScript how to run your Python code. - -That's it! - -For the browser to use PyScript, simply add a ` - - - - - -``` - -There are two ways to tell PyScript how to find your code. - -* With a standard HTML ` -``` - -...and here's a `` tag with inline Python code. - -```html title="A <py-script> tag with inline code" - -import sys -from pyscript import display - - -display(sys.version) - -``` - -The ` - - ``` - - Notice how different interpreters can be used with different - configurations. - - diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 0351907..2a5ca97 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -2,40 +2,152 @@ !!! info - This guide provides technical guidance and exploration of the PyScript - platform. + This guide provides technical guidance and in-depth exploration of + the PyScript platform. - While we endeavour to write clearly, some of the content in this user guide - will not be suitable for beginners. We assume you already have Python - or web development experience. If you're a beginner start with our - [beginner's guide](../beginning-pyscript.md). + We assume you already have Python or web development experience. If + you're new to PyScript, start with our + [beginner's guide](../beginning-pyscript.md) to learn the + fundamentals through a hands-on example. - We [welcome constructive feedback](https://github.com/pyscript/docs/issues). + Once you're comfortable with the basics, return here to explore + PyScript's full capabilities in detail. -Our docs have three aims: +## What you'll learn -1. A [clear overview](what.md) of all things PyScript. -2. [Exploration of PyScript](architecture.md) in substantial technical detail. -3. Demonstration of the features of PyScript working together in - [real-world example applications](../examples.md). +This user guide will teach you how to: -_Read this user guide in full_: it is a short but comprehensive overview of the -PyScript platform. +**Build interactive web applications with Python** - Create rich user +interfaces, handle events, manipulate the DOM, and respond to user input +using idiomatic Python code. -Get involved! Join in the PyScript conversation on our -[discord server](https://discord.gg/HxvBtukrg2). There you'll find core -developers, community contributors and a flourishing forum for those creating -projects with PyScript. Should you wish to engage with the development of -PyScript, you are welcome to contribute via -[the project's GitHub organisation](https://github.com/pyscript). +**Leverage Python's ecosystem in the browser** - Use popular libraries +like NumPy, Pandas, Matplotlib, and thousands more from PyPI directly in +your web applications. -Finally, the example projects referenced in our docs are all freely available -and copiously commented on [pyscript.com](https://pyscript.com). +**Access browser capabilities from Python** - Capture photos and video, +record audio, read and write files, store data locally, and integrate +with web APIs. + +**Create fast, responsive applications** - Use web workers to run Python +code in background threads, keeping your user interface smooth even +during heavy computation. + +**Deploy Python applications anywhere** - Share your work with a simple +URL, no server-side Python required. Your applications run entirely in +the user's browser. + +**Understand PyScript's architecture** - Learn how PyScript bridges +Python and JavaScript, when to use Pyodide vs. MicroPython, and how to +optimise your applications for performance. + +## How to use this guide + +This guide is organised into focused sections that build upon each other. +We recommend reading them in this order: + +1. **[What is PyScript?](what.md)** - Understand the philosophy and + capabilities of the platform. + +2. **[First steps](first-steps.md)** - Learn the basic structure of a + PyScript application. + +3. **[Configuration](configuration.md)** - Discover how to configure + your Python environment, specify packages, and customise PyScript's + behaviour. + +4. **[DOM interaction](dom.md)** - Master the core skill of working with + web page elements using both the FFI and the `pyscript.web` module. + +5. **[Events](events.md)** - Learn to respond to user actions and + browser events with the `@when` decorator and other patterns. + +6. **[Workers](workers.md)** - Understand how to use background threads + for responsive applications. + +7. **[Filesystem](filesystem.md)** and **[Media](media.md)** - Explore + device capabilities like file access, camera, and audio. + +8. **[Architecture](architecture.md)** - Dive deep into how PyScript + works under the hood. + +Of course, you can jump directly to any section that interests you. Each +page is written to be useful on its own, with cross-references to related +topics. + +!!! tip + + New to web development? The [DOM interaction](dom.md) and + [events](events.md) sections are essential reading. They explain + core concepts that apply to all web applications, not just PyScript. + +## Core concepts + +PyScript brings together two powerful ecosystems: Python and the web. +Understanding a few key concepts will help you get the most from the +platform: + +**The FFI (Foreign Function Interface)** - This is how Python and +JavaScript communicate. The FFI automatically translates between Python +and JavaScript objects, letting you use browser APIs directly from +Python code. + +**Pyodide vs. MicroPython** - PyScript supports two Python interpreters. +Pyodide is full CPython compiled to WebAssembly, giving you access to +the entire Python ecosystem. MicroPython is smaller and faster to load, +making it ideal for mobile devices or when you don't need heavy +libraries. + +**The `pyscript` namespace** - This is PyScript's Pythonic API for +working with the web. It includes modules like `pyscript.web` for DOM +manipulation, `pyscript.display` for showing output, and decorators like +`@when` for handling events. + +**Web workers** - Background threads that let you run Python code +without blocking the user interface. Essential for data processing, +simulations, or any CPU-intensive work. + +## Example applications + +Throughout this guide, you'll find working examples that demonstrate +PyScript's features. All examples are complete, runnable applications +that you can explore, modify, and learn from. + +You'll find the example applications in the +[`example-apps` directory in the source code for these docs](https://github.com/pyscript/docs/tree/main/docs/example-apps), +organised by app name. Each example includes all the files you need +(`index.html`, Python code, configuration) plus a README explaining what +it demonstrates and how it works. + +## Get involved + +PyScript is an open source project with a welcoming, vibrant community. + +**Join the conversation** - Our [Discord server](https://discord.gg/HxvBtukrg2) +is the heart of the PyScript community. You'll find core developers, +experienced contributors, and fellow learners ready to help. It's the +best place to ask questions, share your projects, and discuss ideas. + +**Contribute to PyScript** - Whether you're fixing documentation, +reporting bugs, or adding features, contributions are welcome. Visit +[PyScript's GitHub organisation](https://github.com/pyscript) to get +started. + +**Share your work** - Built something interesting with PyScript? We love +to recognise and celebrate community projects. Share your work on +Discord or [get in touch](https://discord.gg/HxvBtukrg2) if you'd like +your project featured in our examples. !!! note - Many of these examples come from contributors in our wonderful - community. We love to recognise, share and celebrate the incredible work - of folks in the PyScript community. If you believe you have a project that - would make a good demonstration, please don't hesitate to - [get in touch](https://discord.gg/HxvBtukrg2). + We [welcome constructive feedback](https://github.com/pyscript/docs/issues) + on these docs. Found something unclear? Have a suggestion? Please let + us know. + +## Ready to dive in? + +Start with [What is PyScript?](what.md) to understand the platform's +philosophy and capabilities, or jump straight to +[First steps](first-steps.md) if you're eager to start coding. + +Welcome to PyScript! \ No newline at end of file diff --git a/docs/user-guide/media.md b/docs/user-guide/media.md index 91e45a7..6dd48d7 100644 --- a/docs/user-guide/media.md +++ b/docs/user-guide/media.md @@ -1,372 +1,236 @@ -# PyScript and Media Devices +# Media -For web applications to interact with cameras, microphones, and other media -devices, there needs to be a way to access these hardware components through the -browser. PyScript provides a media API that enables your Python code to interact -with media devices directly from the browser environment. +Modern web applications often need to interact with cameras, +microphones, and other media devices. PyScript provides a Pythonic +interface to these devices through the `pyscript.media` module, letting +your Python code capture video, record audio, and enumerate available +hardware directly from the browser. -This section explains how PyScript interacts with media devices and how you can -use these capabilities in your applications. +This guide explains how to work with media devices in PyScript, +covering device discovery, stream capture, and practical usage +patterns. -## Media Device Access +## Understanding media access -PyScript interacts with media devices through the browser's [MediaDevices -API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices). This API -provides access to connected media input devices like cameras and microphones, -as well as output devices like speakers. +Media device access in the browser follows strict security and privacy +rules. Your code runs in a sandbox with these constraints: -When using PyScript's media API, it's important to understand: +**User permission is required.** The browser will show a permission +dialog when you first attempt to access cameras or microphones. Users +can grant or deny access, and they can revoke permissions at any time. -1. Media access requires **explicit user permission**. The browser will show a - permission dialog when your code attempts to access cameras or microphones. -2. Media access is only available in **secure contexts** (HTTPS or localhost). -3. All media interactions happen within the **browser's sandbox**, following the - browser's security policies. +**Secure contexts only.** Media access works only over HTTPS or on +localhost. This security requirement prevents malicious sites from +accessing media devices without proper encryption. -## The `pyscript.media` API +**Privacy protections apply.** Device labels may appear as empty strings +until permission is granted. This prevents sites from fingerprinting +users based on their connected hardware before receiving explicit +consent. -PyScript provides a Pythonic interface to media devices through the -`pyscript.media` namespace. This API includes two main components: +These requirements protect users whilst enabling legitimate applications +to work with media devices safely. -1. The `Device` class - represents a media device and provides methods to - interact with it -2. The `list_devices()` function - discovers available media devices +## Listing available devices -### Listing Available Devices - -To discover what media devices are available, use the `list_devices()` function: +The `list_devices()` function discovers what media devices are available +on the user's system: ```python from pyscript.media import list_devices -async def show_available_devices(): - devices = await list_devices() - for device in devices: - print(f"Device: {device.label}, Type: {device.kind}, ID: {device.id}") -# List all available devices -show_available_devices() -``` +# Get all available media devices. +devices = await list_devices() -This function returns a list of `Device` objects, each representing a media -input or output device. Note that the browser will typically request permission -before providing this information. +for device in devices: + print(f"{device.kind}: {device.label}") +``` -### Working with the Camera +Each device has three key properties. The `kind` property indicates +device type: `"videoinput"` for cameras, `"audioinput"` for +microphones, or `"audiooutput"` for speakers. The `label` property +provides a human-readable name like "Built-in Camera" or "External USB +Microphone". The `id` property gives a unique identifier for the device. -The most common use case is accessing the camera to display a video stream: +You can filter devices by type to find specific hardware: ```python -from pyscript import when -from pyscript.media import Device -from pyscript.web import page - -async def start_camera(): - # Get a video stream (defaults to video only, no audio) - stream = await Device.load(video=True) - - # Connect the stream to a video element in your HTML - video_element = page["#camera"][0]._dom_element - video_element.srcObject = stream - - return stream - -# Start the camera -camera_stream = start_camera() +# Find all cameras. +cameras = [d for d in devices if d.kind == "videoinput"] + +# Find all microphones. +microphones = [d for d in devices if d.kind == "audioinput"] + +# Find a specific device by label. +usb_camera = None +for device in devices: + if device.kind == "videoinput" and "USB" in device.label: + usb_camera = device + break ``` -The `Device.load()` method is a convenient way to access media devices without -first listing all available devices. You can specify options to control which -camera is used: +Device labels may be empty strings until the user grants permission to +access media devices. Once permission is granted, labels become +available, helping users understand which hardware is being used. -```python -# Prefer the environment-facing camera (often the back camera on mobile) -stream = await Device.load(video={"facingMode": "environment"}) +## Capturing media streams -# Prefer the user-facing camera (often the front camera on mobile) -stream = await Device.load(video={"facingMode": "user"}) +The `Device.request_stream()` class method requests access to media +devices and returns a stream you can use with HTML video or audio +elements: -# Request specific resolution -stream = await Device.load(video={ - "width": {"ideal": 1280}, - "height": {"ideal": 720} -}) -``` +```python +from pyscript.media import Device +from pyscript.web import page -### Capturing Images from the Camera -To capture a still image from the video stream: +# Request video from the default camera. +stream = await Device.request_stream(video=True) -```python -def capture_image(video_element): - # Get the video dimensions - width = video_element.videoWidth - height = video_element.videoHeight - - # Create a canvas to capture the frame - canvas = document.createElement("canvas") - canvas.width = width - canvas.height = height - - # Draw the current video frame to the canvas - ctx = canvas.getContext("2d") - ctx.drawImage(video_element, 0, 0, width, height) - - # Get the image as a data URL - image_data = canvas.toDataURL("image/png") - - return image_data +# Display it in a video element. +video = page["#my-video"] +video.srcObject = stream ``` -For applications that need to process images with libraries like OpenCV, you -need to convert the image data to a format these libraries can work with: +This triggers a permission dialog the first time it runs. If the user +grants permission, you receive a `MediaStream` object containing the +video feed. If they deny permission, an exception is raised. -```python -import numpy as np -import cv2 - -def process_frame_with_opencv(video_element): - # Get video dimensions - width = video_element.videoWidth - height = video_element.videoHeight - - # Create a canvas and capture the frame - canvas = document.createElement("canvas") - canvas.width = width - canvas.height = height - ctx = canvas.getContext("2d") - ctx.drawImage(video_element, 0, 0, width, height) - - # Get the raw pixel data - image_data = ctx.getImageData(0, 0, width, height).data - - # Convert to numpy array for OpenCV - frame = np.asarray(image_data, dtype=np.uint8).reshape((height, width, 4)) - - # Convert from RGBA to BGR (OpenCV's default format) - frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) - - # Process the image with OpenCV - # ... - - return frame_bgr -``` +You can request audio, video, or both: -### Managing Camera Resources +```python +# Video only (default). +video_stream = await Device.request_stream(video=True) -It's important to properly manage media resources, especially when your -application no longer needs them. Cameras and microphones are shared resources, -and failing to release them can impact other applications or cause unexpected -behavior. +# Audio only. +audio_stream = await Device.request_stream(audio=True, video=False) -### Stopping the Camera +# Both audio and video. +av_stream = await Device.request_stream(audio=True, video=True) +``` -To stop the camera and release resources: +For finer control, specify constraints as dictionaries: ```python -from pyscript.web import page - -def stop_camera(stream): - # Stop all tracks on the stream - if stream: - tracks = stream.getTracks() - for track in tracks: - track.stop() - - # Clear the video element's source - video_element = page["#camera"][0]._dom_element - if video_element: - video_element.srcObject = None +# Request specific video resolution. +stream = await Device.request_stream( + video={"width": 1920, "height": 1080} +) + +# Request high-quality audio with echo cancellation. +stream = await Device.request_stream( + audio={ + "sampleRate": 48000, + "echoCancellation": True, + "noiseSuppression": True + } +) ``` -### Switching Between Cameras +These constraints follow the +[MediaTrackConstraints web standard](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints). +The browser does its best to satisfy your constraints but may fall back +to available settings if exact matches aren't possible. -For devices with multiple cameras, you can implement camera switching: +## Using specific devices + +Sometimes you need to capture from a particular camera or microphone +rather than the default device. List devices first, then request a +stream from the one you want: ```python -from pyscript.media import Device, list_devices -from pyscript.web import page +from pyscript.media import list_devices -class CameraManager: - def __init__(self): - self.cameras = [] - self.current_index = 0 - self.active_stream = None - self.video_element = page["#camera"][0]._dom_element - - async def initialize(self): - # Get all video input devices - devices = await list_devices() - self.cameras = [d for d in devices if d.kind == "videoinput"] - - # Start with the first camera - if self.cameras: - await self.start_camera(self.cameras[0].id) - - async def start_camera(self, device_id=None): - # Stop any existing stream - await self.stop_camera() - - # Start a new stream - video_options = ( - {"deviceId": {"exact": device_id}} if device_id - else {"facingMode": "environment"} - ) - self.active_stream = await Device.load(video=video_options) - - # Connect to the video element - if self.video_element: - self.video_element.srcObject = self.active_stream - - async def stop_camera(self): - if self.active_stream: - tracks = self.active_stream.getTracks() - for track in tracks: - track.stop() - self.active_stream = None - - if self.video_element: - self.video_element.srcObject = None - - async def switch_camera(self): - if len(self.cameras) <= 1: - return - - # Move to the next camera - self.current_index = (self.current_index + 1) % len(self.cameras) - await self.start_camera(self.cameras[self.current_index].id) -``` -## Working with Audio +# Find all cameras. +devices = await list_devices() +cameras = [d for d in devices if d.kind == "videoinput"] -In addition to video, the PyScript media API can access audio inputs: +# Use the second camera if available. +if len(cameras) > 1: + stream = await cameras[1].get_stream() + video = page["#my-video"] + video.srcObject = stream +``` -```python -# Get access to the microphone (audio only) -audio_stream = await Device.load(audio=True, video=False) +The `get_stream()` method on a device instance requests a stream from +that specific device, handling the device ID constraints automatically. -# Get both audio and video -av_stream = await Device.load(audio=True, video=True) -``` +## Example: Photobooth application -## Best Practices +Here's a complete application demonstrating webcam access and still +frame capture: -When working with media devices in PyScript, follow these best practices: + -### Permissions and User Experience +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/photobooth). -1. **Request permissions contextually**: - - Only request camera/microphone access when needed - - Explain to users why you need access before requesting it - - Provide fallback options when permissions are denied +This application requests camera access, displays live video, and +captures still frames using a canvas element. Click "Start Camera" to +begin, then "Capture Photo" to grab the current frame. -2. **Clear user feedback**: - - Indicate when the camera is active - - Provide controls to pause/stop the camera - - Show loading states while the camera is initializing +The technique uses canvas to extract frames from the video stream. The +`drawImage()` method copies the current video frame onto a canvas, +creating a still image you can save or process further. -### Resource Management +## Handling permissions -1. **Always clean up resources**: - - Stop media tracks when they're not needed - - Clear `srcObject` references from video elements - - Be especially careful in single-page applications +Media access requires user permission, and users can deny access or +revoke it later. Always handle these cases gracefully: -2. **Handle errors gracefully**: - - Catch exceptions when requesting media access - - Provide meaningful error messages - - Offer alternatives when media access fails +```python +from pyscript.media import Device -### Performance Optimization -1. **Match resolution to needs**: - - Use lower resolutions when possible - - Consider mobile device limitations - - Adjust video constraints based on the device +try: + stream = await Device.request_stream(video=True) + # Use the stream. + video = page["#camera"] + video.srcObject = stream +except Exception as e: + # Permission denied or device not available. + print(f"Could not access camera: {e}") + # Show a message to the user explaining what happened. +``` -2. **Optimize image processing**: - - Process frames on demand rather than continuously - - Use efficient algorithms - - Consider downsampling for faster processing +Consider providing fallback content or alternative functionality when +media access isn't available. This improves the experience for users who +deny permission or lack the necessary hardware. -## Example Application: Simple Camera Capture +## Stream management -Here's a simplified example that shows how to capture and display images from a -camera: +Media streams use system resources. Stop streams when you're finished to +free up cameras and microphones: ```python -from pyscript import when, window -from pyscript.media import Device -from pyscript.web import page +# Get stream. +stream = await Device.request_stream(video=True) -class CameraCapture: - def __init__(self): - # Get UI elements - self.video = page["#camera"][0] - self.video_element = self.video._dom_element - self.capture_button = page["#capture-button"] - self.snapshot = page["#snapshot"][0] - - # Start camera - self.initialize_camera() - - async def initialize_camera(self): - # Prefer environment-facing camera on mobile devices - stream = await Device.load(video={"facingMode": "environment"}) - self.video_element.srcObject = stream - - def take_snapshot(self): - """Capture a frame from the camera and display it""" - # Get video dimensions - width = self.video_element.videoWidth - height = self.video_element.videoHeight - - # Create canvas and capture frame - canvas = window.document.createElement("canvas") - canvas.width = width - canvas.height = height - - # Draw the current video frame to the canvas - ctx = canvas.getContext("2d") - ctx.drawImage(self.video_element, 0, 0, width, height) - - # Convert the canvas to a data URL and display it - image_data_url = canvas.toDataURL("image/png") - self.snapshot.setAttribute("src", image_data_url) - -# HTML structure needed: -# -# -# - -# Usage: -# camera = CameraCapture() -# -# @when("click", "#capture-button") -# def handle_capture(event): -# camera.take_snapshot() +# Use it... +video = page["#camera"] +video.srcObject = stream + +# Later, stop all tracks. +for track in stream.getTracks(): + track.stop() ``` -This example demonstrates: -- Initializing a camera with the PyScript media API -- Accessing the camera stream and displaying it in a video element -- Capturing a still image from the video stream when requested -- Converting the captured frame to an image that can be displayed +Stopping tracks releases the hardware, allowing other applications to +use the devices and conserving battery life on mobile devices. + +## What's next -This simple pattern can serve as the foundation for various camera-based -applications and can be extended with image processing libraries as needed for -more complex use cases. +Now that you understand media device access, explore these related +topics: +**[Workers](workers.md)** - Display content from background threads +(requires explicit `target` parameter). -## Conclusion +**[Filesystem](filesystem.md)** - Learn more about the virtual +filesystem and how the `files` option works. -The PyScript media API provides a powerful way to access and interact with -cameras and microphones directly from Python code running in the browser. By -following the patterns and practices outlined in this guide, you can build -sophisticated media applications while maintaining good performance and user -experience. +**[FFI](ffi.md)** - Understand how JavaScript modules integrate with +Python through the foreign function interface. -Remember that media access is a sensitive permission that requires user consent -and should be used responsibly. Always provide clear indications when media -devices are active and ensure proper cleanup of resources when they're no longer -needed. +**[Offline](offline.md)** - Use PyScript while not connected to the internet. \ No newline at end of file diff --git a/docs/user-guide/offline.md b/docs/user-guide/offline.md index 919f387..bebde43 100644 --- a/docs/user-guide/offline.md +++ b/docs/user-guide/offline.md @@ -1,88 +1,90 @@ -# Use PyScript Offline +# Running Offline -Sometimes you want to run PyScript applications offline. +PyScript applications typically load from a Content Delivery Network +or webserver, fetching core files and Python interpreters over the internet. +This works well when you have reliable network access, but sometimes you need +applications to run offline - in air-gapped environments, on local +networks, or where internet connectivity isn't available or is patchy. -Both PyScript core and the interpreter used to run code need to be served with -the application itself. The two requirements needed to create an offline -version of PyScript are: +Running PyScript offline means bundling everything your application +needs locally. This guide explains how to package PyScript core and +Python interpreters so your applications work without network access. -1. Download and include PyScript core. -2. Download and include the Python interpreters used in your application. +## What you need -## Get PyScript core +An offline PyScript application requires two components available +locally: -You have two choices: +The first is PyScript core itself - the `core.js` file and associated +resources that provide the runtime. This is what loads interpreters, +manages execution, and bridges Python to the browser. - 1. **Build from source**. Clone the repository, install dependencies, then - build and use the content found in the `./dist/` folder. - 2. **Grab the npm package**. For simplicity this is the method we currently - recommend as the easiest to get started. +The second is the Python interpreter your application uses - either +Pyodide or MicroPython. These are substantial files containing the +entire Python runtime compiled to WebAssembly. -In the following instructions, we assume the existence of a folder called -`pyscript-offline`. All the necessary files needed to use PyScript offline will -eventually find their way in there. +If your application imports Python packages, those need to be bundled +locally as well. For Pyodide applications using numpy, pandas, or other +libraries, the package files must also be available offline. -In your computer's command line shell, create the `pyscript-offline` folder -like this: +## Shortcut -```sh -mkdir -p pyscript-offline -``` +Helpfully, since the end of 2025, all +[releases of PyScript](https://pyscript.net/releases/2025.11.2/) +have an associated `offline` +zip file containing everything you need. Just download it, unpack it and +start to modify the content of the `index.html` found therein to your needs. -Now change into the newly created directory: +Read on, if you want to modify or learn how such assets are created. -```sh -cd pyscript-offline -``` +## Getting PyScript core -### PyScipt core from source +You have two ways to obtain PyScript core files. -Build PyScript core by cloning the project repository and follow the -instructions in our [developer guide](../developers.md) +### Using npm (recommended) -Once completed, copy the `dist` folder, that has been created by the build -step, into your `pyscript-offline` folder. +The simplest approach uses npm to download the distribution: -### PyScript core from `npm` +```sh +# Create your project directory. +mkdir pyscript-offline +cd pyscript-offline -Ensure you are in the `pyscript-offline` folder created earlier. +# Create a package.json file. +echo '{}' > package.json -Create a `package.json` file. Even an empty one with just `{}` as content will -suffice. This is needed to make sure our folder will include the local -`npm_modules` folder instead of placing assets elsewhere. Our aim is to ensure -everything is in the same place locally. +# Install PyScript core. +npm i @pyscript/core -```sh -# only if there is no package.json, create one -echo '{}' > ./package.json +# Copy distribution files to your public directory. +mkdir -p public +cp -R node_modules/@pyscript/core/dist public/pyscript ``` -Assuming you have -[npm installed on your computer](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), -issue the following command in the `pyscript-offline` folder to install the -PyScript core package. +This gives you a `public/pyscript` directory containing everything +needed to run PyScript locally. -``` -# install @pyscript/core -npm i @pyscript/core -``` +### Building from source -Now the folder should contain a `node_module` folder in it, and we can copy the -`dist` folder found within the `@pyscript/core` package wherever we like. +For development builds or customisation, clone and build the repository: ```sh -# create a public folder to serve locally -mkdir -p public +# Clone the repository. +git clone https://github.com/pyscript/pyscript.git +cd pyscript -# move @pyscript/core dist into such folder -cp -R ./node_modules/@pyscript/core/dist ./public/pyscript +# Follow build instructions from the developer guide. +# Once built, copy the dist folder. +cp -R dist ../pyscript-offline/public/pyscript ``` -That's almost it! +See the [developer guide](../developers.md) for detailed build +instructions. -## Set up your application +## Setting up your application -Simply create a `./public/index.html` file that loads the local PyScript: +Create an HTML file that loads PyScript from local paths rather than the +CDN: ```html @@ -97,55 +99,39 @@ Simply create a `./public/index.html` file that loads the local PyScript: ``` -Run this project directly (after being sure that `index.html` file is saved -into the `public` folder): +Save this as `public/index.html` and serve it locally: ```sh -python3 -m http.server -d ./public/ +python3 -m http.server -d public/ ``` -If you would like to test `worker` features, try instead: - -```sh -npx mini-coi ./public/ -``` +Open `http://localhost:8000` in your browser. PyScript loads from local +files with no network requests. -## Download a local interpreter +## Getting MicroPython -PyScript officially supports *MicroPython* and *Pyodide* interpreters, so let's -see how to get a local copy for each one of them. - -### Local MicroPython - -Similar to `@pyscript/core`, we can also install *MicroPython* from *npm*: +MicroPython is available through npm: ```sh +# Install the MicroPython package. npm i @micropython/micropython-webassembly-pyscript -``` -Our `node_modules` folder should contain a `@micropython` one and from there we -can move relevant files into our `public` folder. +# Create target directory. +mkdir -p public/micropython -Let's be sure we have a target for that: - -```sh -# create a folder in our public space -mkdir -p ./public/micropython - -# copy related files into such folder -cp ./node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./public/micropython/ +# Copy interpreter files. +cp node_modules/@micropython/micropython-webassembly-pyscript/micropython.* public/micropython/ ``` -The folder should contain at least both `micropython.mjs` and -`micropython.wasm` files. These are the files to use locally via a dedicated -config. +This copies `micropython.mjs` and `micropython.wasm` to your public +directory. Configure your HTML to use these local files: ```html @@ -158,42 +144,45 @@ config. + interpreter = "/micropython/micropython.mjs" -``` +``` -### Local Pyodide +The `interpreter` configuration tells PyScript where to find the local +MicroPython files. -Remember, Pyodide uses `micropip` to install third party packages. While the -procedure for offline Pyodide is very similar to the one for MicroPython, -if we want to use 3rd party packages we also need to have these available -locally. We'll start simple and cover such packaging issues at the end. +## Getting Pyodide + +Pyodide is also available through npm: ```sh -# locally install the pyodide module +# Install Pyodide. npm i pyodide -# create a folder in our public space -mkdir -p ./public/pyodide +# Create target directory. +mkdir -p public/pyodide -# move all necessary files into that folder -cp ./node_modules/pyodide/pyodide* ./public/pyodide/ -cp ./node_modules/pyodide/python_stdlib.zip ./public/pyodide/ +# Copy all necessary files. +cp node_modules/pyodide/pyodide* public/pyodide/ +cp node_modules/pyodide/python_stdlib.zip public/pyodide/ ``` -Please **note** that the `pyodide-lock.json` file is needed, so please don't -change that `cp` operation as all `pyodide*` files need to be moved. +!!! info + + Make sure to copy all files matching `pyodide*`, including + `pyodide-lock.json`. This lock file is essential for Pyodide to + function correctly. -At this point, all we need to do is to change the configuration on our *HTML* -page to use *pyodide* instead: +Configure your HTML to use local Pyodide: ```html @@ -206,50 +195,47 @@ page to use *pyodide* instead: + interpreter = "/pyodide/pyodide.mjs" ``` -## Wrap up - -That's basically it! - -Disconnect from the internet, run the local server, and the page will still -show that very same `Hello from PyScript` message. - -## Local Pyodide packages +## Bundling Pyodide packages -Finally, we need the ability to install Python packages from a local source -when using Pyodide. - -Put simply, we use the packages bundle from -[pyodide releases](https://github.com/pyodide/pyodide/releases/tag/0.26.2). +If your application uses Python packages like numpy or pandas, you need +the Pyodide package bundle. Download it from the +[Pyodide releases page](https://github.com/pyodide/pyodide/releases). !!! warning - This bundle is more than 200MB! + The complete package bundle exceeds 200MB. It contains all packages + available in Pyodide. Pyodide loads packages on demand, so you only + download what your code actually imports, but the entire bundle must + be available locally. - It contains each package that is required by Pyodide, and Pyodide will only - load packages when needed. +Download and extract the bundle for your Pyodide version (e.g., +`pyodide-0.26.2.tar.bz2`): -Once downloaded and extracted (we're using version `0.26.2` in this example), -we can simply copy the files and folders inside the `pyodide-0.26.2/pyodide/*` -directory into our `./public/pyodide/*` folder. +```sh +# Download the bundle. +wget https://github.com/pyodide/pyodide/releases/download/0.26.2/pyodide-0.26.2.tar.bz2 -Feel free to either skip or replace the content, or even directly move the -`pyodide` folder inside our `./public/` one. +# Extract it. +tar -xjf pyodide-0.26.2.tar.bz2 -Now use any package available in via the Pyodide bundle. +# Copy package files to your public directory. +cp -R pyodide-0.26.2/pyodide/* public/pyodide/ +``` -For example: +Now use packages in your application: ```html @@ -268,16 +254,66 @@ For example:
``` -We should now be able to read `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` on the -page *even* if we disconnect from the Internet. +The page will display `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` even with no +internet connection. Pyodide loads pandas and all dependencies from your +local package bundle. + +## Testing offline operation + +To verify everything works offline, disconnect from the internet and +reload your application. If it loads and runs correctly, you've +successfully configured offline operation. + +You can also test using browser developer tools. Open the Network tab, +enable "Offline" mode, and reload. All resources should load from cache +or local files with no network errors. + +## Serving with workers + +If your application uses workers, you need a server that sets +Cross-Origin Isolation (COI) headers. These headers enable +`SharedArrayBuffer` and other features required for worker support. Use +`mini-coi` instead of Python's simple server: + +```sh +# Install mini-coi if needed. +npm i -g mini-coi + +# Serve with COI headers enabled. +npx mini-coi public/ +``` + +The `mini-coi` tool automatically sets the necessary headers to enable +worker functionality. + +## What's next + +Now that you understand offline deployment, explore these related +topics: + +**[Architecture guide](architecture.md)** - provides technical details about +how PyScript implements workers using PolyScript and Coincident if you're +interested in the underlying mechanisms. + +**[Workers](workers.md)** - Display content from background threads +(requires explicit `target` parameter). + +**[Filesystem](filesystem.md)** - Learn more about the virtual +filesystem and how the `files` option works. + +**[FFI](ffi.md)** - Understand how JavaScript modules integrate with +Python through the foreign function interface. -That's it! +**[Media](media.md)** - Capture photos and video with the camera or +record audio with your microphone. \ No newline at end of file diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md index e6727b3..9f922d6 100644 --- a/docs/user-guide/plugins.md +++ b/docs/user-guide/plugins.md @@ -1,209 +1,248 @@ # Plugins -PyScript offers a plugin API _so anyone can extend its functionality and -share their modifications_. - -PyScript only supports plugins written in Javascript (although causing the -evaluation of bespoke Python code can be a part of such plugins). The plugin's -JavaScript code should be included on the web page via a -` - - - - - - - + + + + + + + + + + + + + + ``` + +The output shows the lifecycle sequence: ready hooks fire first, then +before run hooks, then injected code before, then the actual script, +then injected code after, and finally after run hooks. + +## Plugin use cases + +Plugins enable many customisations. You might create plugins to log +execution for debugging, inject analytics or telemetry code, modify +interpreter behaviour, provide library bootstrapping, implement custom +security checks, or add domain-specific features. + +The terminal and editor features in PyScript are themselves implemented +as plugins, demonstrating the power and flexibility of the plugin system. + +## What's next + +Now that you understand plugins, explore these related topics: + +**[Terminal](terminal.md)** - Use the alternative REPL-style +interface for interactive Python sessions. + +**[Editor](editor.md)** - Create interactive Python coding environments in +web pages with the built-in code editor. + +**[PyGame](pygame-ce.md)** - Use PyGame-CE with PyScript, covering the +differences from traditional PyGame development and techniques for making +games work well in the browser. \ No newline at end of file diff --git a/docs/user-guide/pygame-ce.md b/docs/user-guide/pygame-ce.md index 6dccdde..88ad238 100644 --- a/docs/user-guide/pygame-ce.md +++ b/docs/user-guide/pygame-ce.md @@ -1,72 +1,75 @@ # PyGame Support -!!! Danger - - **Support for PyGame-CE is experimental** and its behaviour is likely to - change as we get feedback and bug reports from the community. - - Please bear this in mind as you try PyGame-CE with PyScript, and all - feedback, bug reports and constructive critique is welcome via discord - or GitHub. - - -[PyGameCE](https://pyga.me/) is a Python library for building powerful games -(so says their website). They also say, to get started you just need to -`pip install pygame-ce`. - -Thanks to work in the upstream [Pyodide project](https://pyodide.org/) -PyGame-CE is available in PyScript and to get started all you need to do is: -`` Now you don't even need to -`pip install` the library! It comes with PyScript by default, and you can share -your games via a URL! - -!!! Info - - Please refer to - [PyGame-CE's extensive documentation](https://pyga.me/docs/) for how to - create a game. Some things may not work because we're running in a - browser context, but play around and let us know how you get on. - -## Getting Started - -Here are some notes on using PyGame-CE specifically in a browser context with -pyscript versus running locally per -[PyGame-CE's documentation](https://pyga.me/docs/). - -1. You can use [pyscript.com](https://pyscript.com) as mentioned in - [Beginning PyScript](../beginning-pyscript.md) for an easy starting - environment. -2. Pyscript's PyGame-CE is under development, so make sure to use the latest - version by checking the `index.html` and latest version on this website. If - using [pyscript.com](https://pyscript.com), the latest version is not always - used in a new project. -3. The game loop needs to allow the browser to run to update the canvas used as - the game's screen. In the simplest projects, the quickest way to do that is - to replace `clock.tick(fps)` with `await asyncio.sleep(1/fps)`, but there - are better ways (discussed later). -4. If you have multiple Python source files or media such as images or sounds, - you need to use the [config attribute](configuration.md) to load the - files into the PyScript environment. The below example shows how to do this. -5. The integrated version of Python and PyGame-CE may not be the latest. In the - browser's console when PyGame-CE starts you can see the versions, and for - example if 2.4.1 is included, you can't use a function marked in the - documentation as "since 2.5". - -### Example - -This is the example quickstart taken from the [Python Pygame -Introduction](https://pyga.me/docs/tutorials/en/intro-to-pygame.html) on the -PyGame-CE website, modified only to add `await asyncio.sleep(1/60)` (and the -required `import asyncio`) to limit the game to roughly 60 fps. - -Note: since the `await` is not in an `async` function, it cannot run using -Python on your local machine, but a solution is -[discussed later](#running-locally). - - -```python title="quickstart.py" +PyScript includes experimental support for +[PyGame Community Edition](https://pyga.me/), a Python library for +building games. PyGame-CE runs in the browser through PyScript, letting +you share games via URL without requiring players to install Python or +any dependencies. + +This guide explains how to use PyGame-CE with PyScript, covering the +differences from traditional PyGame development and techniques for +making games work well in the browser. + +!!! warning + + PyGame-CE support is experimental. Behaviour may change based on + community feedback and bug reports. Please share your experiences + via Discord or GitHub to help improve this feature. + +## Quick start + +Create a PyGame-CE application by using the `py-game` script type: + +```html + +``` + +PyGame-CE loads automatically - no pip installation needed. Your game +runs in the browser and can be shared via URL like any other web page. + +Refer to [PyGame-CE's documentation](https://pyga.me/docs/) for game +development techniques. Most features work in the browser, though some +may behave differently due to the browser environment. + +## Browser considerations + +PyGame-CE in the browser differs from local development in several key +ways. Understanding these differences helps you write games that work +well in both environments. + +The browser needs regular opportunities to update the canvas displaying +your game. Replace `clock.tick(fps)` with `await asyncio.sleep(1/fps)` +to give the browser time to render. Better timing techniques exist and +are covered later in this guide. + +Media files like images and sounds must be explicitly loaded using +PyScript's configuration system. Use the `files` section in your +configuration to make assets available. + +Python and PyGame-CE versions in the browser may lag behind the latest +releases. Check the browser console when PyGame-CE starts to see which +versions are available. Functions marked "since 2.5" in the +documentation won't work if version 2.4.1 is bundled. + +## Complete example + +Here's a complete bouncing ball game demonstrating PyGame-CE in the +browser: + + + +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/bouncing-ball). + +This example shows the essential pattern for PyGame-CE in PyScript. The +game uses `await asyncio.sleep(1/60)` to yield control to the browser +for canvas updates. + +The Python code: + +```python import asyncio -import sys, pygame +import sys +import pygame pygame.init() @@ -80,7 +83,8 @@ ballrect = ball.get_rect() while True: for event in pygame.event.get(): - if event.type == pygame.QUIT: sys.exit() + if event.type == pygame.QUIT: + sys.exit() ballrect = ballrect.move(speed) if ballrect.left < 0 or ballrect.right > width: @@ -94,228 +98,225 @@ while True: await asyncio.sleep(1/60) ``` -To run this game with PyScript, use the following HTML file, ensuring a call -to the Python program and a `` element where the graphics -will be placed. Make sure to update the pyscript release to the latest version. +The only addition to standard PyGame code is `await asyncio.sleep(1/60)`, +which gives the browser time to render. The `await` at the top level +works in PyScript's async context but won't run locally without +modification (covered below). + +The HTML file needs a canvas element and the script tag: -```html title="index.html" +```html - - PyScript Pygame-CE Quickstart + PyScript PyGame-CE Example - - + + - - + + ``` -!!! Info +!!! info - The `style="image-rendering: pixelated` on the canvas preserves the - pixelated look on high-DPI screens or when zoomed-in. Remove it to have a - "smoothed" look. - -Lastly, you need to define the `pyscript.toml` file to expose any files that -your game loads -- in this case, `intro_ball.gif` -[(download from pygame GitHub)](https://github.com/pygame-community/pygame-ce/blob/80fe4cb9f89aef96f586f68d269687572e7843f6/docs/reST/tutorials/assets/intro_ball.gif?raw=true). + The `style="image-rendering: pixelated"` preserves the pixelated + look on high-DPI screens. Remove it for smoothed rendering. -```toml title="pyscript.toml" +The configuration file lists game assets: + +```toml [files] "intro_ball.gif" = "" ``` -Now you only need to serve the 3 files to a browser. If using -[pyscript.com](https://pyscript.com) you only need to ensure the content of the -files, click save then run and view the preview tab. Or, if you are on a machine -with Python installed you can do it from a command line running in the same -directory as the project: - -``` -python -m http.server -b 127.0.0.1 8000 -``` - -This will start a website accessible only to your machine (`-b 127.0.0.1` limits -access only to "localhost" -- your own machine). After running this, you can -visit [http://localhost:8000/](http://localhost:8000/) to run the game in your -browser. +Download `intro_ball.gif` from the +[PyGame-CE repository](https://github.com/pygame-community/pygame-ce/blob/main/docs/reST/tutorials/assets/intro_ball.webp?raw=true). -Congratulations! Now you know the basics of updating games to run in PyScript. -You can continue to develop your game in the typical PyGame-CE way. +## Running locally and in browser -## Running Locally +The top-level `await` in the example isn't valid in standard Python (it +should be inside an async function). PyScript provides an async context +automatically, but local Python doesn't. -Placing an `await` call in the main program script as in the example is not -technically valid Python as it should be in an `async` function. In the -environment executed by PyScript, the code runs in an `async` context so this -works; however, you will notice you cannot run the `quickstart.py` on your -local machine with Python. To fix that, you need to add just a little more -code: - -Place the entire game in a function called `run_game` so that function can be -declared as `async`, allowing it to use `await` in any environment. Import the -`asyncio` package and add the `try ... except` code at the end. Now when running -in the browser, `asyncio.create_task` is used, but when running locally -`asyncio.run` is used. Now you can develop and run locally but also support -publish to the web via PyScript. +Wrap your game in an async function and use a try-except block to detect +the environment: ```python import asyncio -import sys, pygame +import sys +import pygame + async def run_game(): + """ + Main game function. + """ pygame.init() - # Game init ... + # Initialise game state... + size = width, height = 320, 240 + screen = pygame.display.set_mode(size) while True: for event in pygame.event.get(): - if event.type == pygame.QUIT: sys.exit() - - # Game logic ... + if event.type == pygame.QUIT: + sys.exit() + + # Game logic... await asyncio.sleep(1/60) + try: - asyncio.get_running_loop() # succeeds if in async context + # Check if we're in an async context (PyScript). + asyncio.get_running_loop() asyncio.create_task(run_game()) except RuntimeError: - asyncio.run(run_game()) # start async context as we're not in one + # No async context (local Python). + asyncio.run(run_game()) ``` -!!! Info +This pattern works in both environments. PyScript uses `create_task()`, +local Python uses `asyncio.run()`. Now you can develop locally and +publish to the web without changing code. - In the web version, the `sys.exit()` was never used because the `QUIT` - event is not generated, but in the local version, responding to the event - is mandatory. - -## Advanced Timing - -While the `await asyncio.sleep(1/60)` is a quick way to approximate 60 FPS, -like all sleep-based timing methods in games this is not precise. Generating -the frame itself takes time, so sleeping 1/60th of a second means total frame -time is longer and actual FPS will be less than 60. - -A better way is to do this is to run your game at the same frame rate as the -display (usually 60, but can be 75, 100, 144, or higher on some displays). When -running in the browser, the proper way to do this is with the JavaScript API -called `requestAnimationFrame`. Using the FFI (foreign function interface) -capabilities of PyScript, we can request the browser's JavaScript runtime to -call the game. The main issue of this method is it requires work to separate the -game setup from the game's execution, which may require more advanced Python -code such as `global` or `class`. However, one benefit is that the `asyncio` -usages are gone. - - -When running locally, you get the same effect from the `vsync=1` parameter on -`pygame.display.set_mode` as `pygame.display.flip()` will pause until the screen -has displayed the frame. In the web version, the `vsync=1` will do nothing, -`flip` will not block, leaving the browser itself to control the timing using -`requestAnimationFrame` by calling `run_one_frame` (via `on_animation_frame`) -each time the display updates. - -Additionally, since frame lengths will be different on each machine, we need to -account for this by creating and using a `dt` (delta time) variable by using a -`pygame.time.Clock`. We update the speed to be in pixels per second and multiply -by `dt` (in seconds) to get the number of pixels to move. - -The code will look like this: +!!! info + + The `pygame.QUIT` event never fires in the browser version, but + handling it is mandatory for local execution where closing the + window generates this event. + +## Precise frame timing + +The `await asyncio.sleep(1/60)` approach approximates 60 FPS but isn't +precise. Frame rendering takes time, so sleeping 1/60th of a second +results in actual FPS below 60. + +Better timing synchronises with the display refresh rate using +`requestAnimationFrame` in the browser and `vsync=1` locally. This +requires separating setup from the game loop: ```python -import sys, pygame +import sys +import pygame pygame.init() size = width, height = 320, 240 -speed = pygame.Vector2(150, 150) # use Vector2 so we can multiply with dt +speed = pygame.Vector2(150, 150) black = 0, 0, 0 -screen = pygame.display.set_mode(size, vsync=1) # Added vsync=1 +screen = pygame.display.set_mode(size, vsync=1) ball = pygame.image.load("intro_ball.gif") ballrect = ball.get_rect() -clock = pygame.time.Clock() # New clock defined +clock = pygame.time.Clock() + def run_one_frame(): + """ + Execute one frame of the game. + """ for event in pygame.event.get(): - if event.type == pygame.QUIT: sys.exit() - - # in this 300 is for maximum frame rate only, in case vsync is not working + if event.type == pygame.QUIT: + sys.exit() + + # Delta time for frame-rate independence. dt = clock.tick(300) / 1000 - - ballrect.move_ip(speed * dt) # use move_ip to avoid the need for "global" - # Remaining game code unchanged ... + ballrect.move_ip(speed * dt) + + # Bounce logic... + if ballrect.left < 0 or ballrect.right > width: + speed.x = -speed.x + if ballrect.top < 0 or ballrect.bottom > height: + speed.y = -speed.y + + screen.fill(black) + screen.blit(ball, ballrect) pygame.display.flip() - -# PyScript-specific code to use requestAnimationFrame in browser + +# Browser: use requestAnimationFrame. try: - from pyscript import window - from pyscript import ffi - # Running in PyScript - def on_animation_frame(dt): - # For consistency, we use dt from pygame's clock even in browser + from pyscript import window, ffi + + def on_animation_frame(timestamp): + """ + Called by browser for each frame. + """ run_one_frame() window.requestAnimationFrame(raf_proxy) + raf_proxy = ffi.create_proxy(on_animation_frame) on_animation_frame(0) - + except ImportError: - # Local Execution + # Local: use while loop with vsync. while True: run_one_frame() ``` -A benefit of `vsync` / `requestAnimationFrame` method is that if the game is -running too slowly, frames will naturally be skipped. A drawback is that in the -case of skipped frames and different displays, `dt` will be different. This can -cause problems depending on your game's physics code; the potential solutions -are not unique to the PyScript situation and can be found elsewhere online as an -exercise for the reader. For example, the above example on some machines the -ball will get "stuck" in the sides. In case of issues the `asyncio.sleep` method -without `dt` is easier to deal with for the beginning developer. - -## How it works - -When a `` element is found on the page a -Pyodide instance is bootstrapped with the `pygame-ce` package already included. -Differently from other scripts, `py-game` cannot currently work through a -worker and it uses an optional target attribute to define the `` -element id that will be used to render the game. If no target attribute is -defined, the script assumes there is a `` element already -on the page. - -A config attribute can be specified to add extra packages or bring in additional -files such as images and sounds but right now that's all it can do. - -!!! Info - - Sometimes you need to gather text based user input when starting a game. - The usual way to do this is via the builtin `input` function. - - Because PyGame-CE **only runs on the main thread**, the only way to block - your code while it waits for user `input` is to use a - [JavaScript prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) - instead of input typed in via a terminal. PyScript handles this - automatically for you if you use the `input` function. - -This is an experiment, but: - -* It is possible to use regular PyScript to load the pygame-ce package and use - all the other features. But there be dragons! This helper simply allows - multiple games on a page and forces game logic to run on the main thread to - reduce confusion around attributes and features when the `pygame-ce` package - is meant to be used. Put simply, we make it relatively safe and easy to use. -* The fact `pygame-ce` is the default "game engine" does not mean in the future - PyScript won't have other engines also available. -* Once again, as this is an experiment, we welcome any kind of feedback, - suggestions, hints on how to improve or reports of what's missing. - -Other than that, please go make and share wonderful games. We can't wait to see -what you come up with. +This synchronises with display refresh (usually 60Hz, but can be higher). +Delta time (`dt`) accounts for frame rate variations between machines. +Speed is now in pixels per second, multiplied by `dt` to get movement +per frame. + +The `vsync=1` parameter makes `flip()` block until the display updates +locally. In the browser, `vsync=1` does nothing - instead, +`requestAnimationFrame` controls timing. + +Note that variable frame rates can cause physics issues. The ball might +get stuck in walls if frame skipping occurs. For beginners, the simpler +`asyncio.sleep` method may be easier despite being less precise. + +## How PyGame-CE integration works + +The `py-game` script type bootstraps Pyodide with PyGame-CE +pre-installed. Unlike regular scripts, PyGame-CE always runs on the main +thread and cannot use workers. + +The `target` attribute specifies which canvas element displays the game. +If omitted, PyScript assumes a `` element exists. + +Configuration through the `config` attribute adds packages or files like +images and sounds. This is currently the only configuration PyGame-CE +scripts support. + +!!! info + + The `input()` function works in PyGame-CE but uses the browser's + native `prompt()` dialog. Since PyGame-CE runs on the main thread, + this is the only way to block for user input. PyScript handles this + automatically when you call `input()`. + +## Experimental status + +PyGame-CE support is experimental but functional. You can load +`pygame-ce` manually through regular PyScript if needed, but the +`py-game` script type simplifies multi-game pages and ensures game logic +runs on the main thread where PyGame-CE expects it. + +Future PyScript versions may include other game engines alongside +PyGame-CE. We welcome feedback, suggestions, and bug reports to improve +this feature. + +## What's next + +Now that you understand PyGame-CE support, explore these related topics: + +**[Terminal](terminal.md)** - Use the alternative REPL-style +interface for interactive Python sessions. + +**[Editor](editor.md)** - Create interactive Python coding environments in +web pages with the built-in code editor. + +**[Plugins](plugins.md)** - Understand the plugin system, lifecycle hooks, +and how to write plugins that integrate with PyScript. \ No newline at end of file diff --git a/docs/user-guide/running-offline.md b/docs/user-guide/running-offline.md deleted file mode 100644 index 7e96989..0000000 --- a/docs/user-guide/running-offline.md +++ /dev/null @@ -1,235 +0,0 @@ -# Running PyScript Offline - -Although users will want to create and share PyScript apps on the internet, there are cases when user want to run PyScript applications offline, in an airgapped fashion. This means that both PyScript core and the interpreter used to run code need to be served with the application itself. In short, the 2 main explicit tasks needed to create an offline PyScript application are: - -* download and include PyScript core (`core.js`) -* download and include the [Python] interpreters you want to use in your Application - -## Downloading and Including PyScript's `core.js` - -There are at least 2 ways to use PyScript offline: - - * by **cloning the repository**, then building and installing dependencies and then run and then reach the `./dist/` folder - * by **grabbing the npm package** which for simplicity sake will be the method used here at least until we find a better place to *pin* our *dist* folder via our CDN and make the procedure even easier than it is now - -In the examples below, we'll assume we are creating a PyScript Application folder called `pyscript-offline` and we'll add all the necessary files to the folder. - -First of all, we are going to create a `pyscript-offline` folder as reference. - -```sh -mkdir -p pyscript-offline -cd pyscript-offline -``` - -### Adding core by Cloning the Repository - -You can build all the PyScript Core files by cloning the project repository and building them yourself. To do so, build the files by following the instructions in our [developer guide](/developers) - -Once you've run the `build` command, copy the `build` folder that has been created into your `pyscript-offline` folder. - -### Adding core by Installing `@pyscript/core` Locally - -First of all, ensure you are in the folder you would like to test PyScirpt locally. In this case, the `pyscript-offline` folder we created earlier. - -Once within the folder, be sure there is a `package.json` file. Even an empty one with just `{}` as content would work. -This is needed to be sure the folder will include locally the `npm_modules` folder instead of placing the package in the parent folder, if any. - -```sh -# only if there is no package.json, create one -echo '{}' > ./package.json - -# install @pyscript/core -npm i @pyscript/core -``` - -At this point the folder should contain a `node_module` in it and we can actually copy its `dist` folder wherever we like. - -```sh -# create a public folder to serve locally -mkdir -p public - -# move @pyscript/core dist into such folder -cp -R ./node_modules/@pyscript/core/dist ./public/pyscript -``` - -## Setting up your application - -Once you've added PyScript code following one of the methods above, that's almost it! We are half way through our goal but we can already create a `./public/index.html` file that loads the project: - -```html - - - - - - PyScript Offline - - - - - - - -``` - -To run this project directly, after being sure that `index.html` file is saved into the `public` folder, you can try: - -```sh -python3 -m http.server -d ./public/ -``` - -Alternatively, if you would like to test also `worker` features, you can try instead: - -```sh -npx static-handler --coi ./public/ -``` -## Downloading and Setting up a Local Interpreter - -Good news! We are almost there. Now that we've: - -* downloaded PyScript locally -* created the skeleton of an initial PyScript App - -we need to download and setup up an interpreter. PyScript officially supports *MicroPython* and *Pyodide* interpreters, so let's see how to do that for each one of them. - -### Download MicroPython locally - -Similarly to what we did for `@pyscript/core`, we can also install *MicroPython* from *npm*: - -```sh -npm i @micropython/micropython-webassembly-pyscript -``` - -Our `node_modules` folder now should contain a `@micropython` one and from there we can move relevant files into our `public` folder, but let's be sure we have a target for that: - -```sh -# create a folder in our public space -mkdir -p ./public/micropython - -# copy related files into such folder -cp ./node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./public/micropython/ -``` - -That folder should contain at least both `micropython.mjs` and `micropython.wasm` files and these are the files we are going to use locally via our dedicated config. - -```html - - - - - - PyScript Offline - - - - - - interpreter = "/micropython/micropython.mjs" - - - - -``` - -### Install Pyodide locally - -Currently there is a difference between MicroPython and Pyodide: the former does not have (*yet*) a package manager while the latest does, it's called *micropip*. - -This is important to remember because while the procedure to have *pyodide* offline is very similar to the one we've just seen, if we want to use also 3rd party packages we also need to have these running locally ... but let's start simple: - -```sh -# install locally the pyodide module -npm i pyodide - -# create a folder in our public space -mkdir -p ./public/pyodide - -# move all necessary files into that folder -cp ./node_modules/pyodide/pyodide* ./public/pyodide/ -cp ./node_modules/pyodide/python_stdlib.zip ./public/pyodide/ -``` - -Please **note** that also `pyodide-lock.json` file is needed so please don't change that `cp` operation as all `pyodide*` files need to be moved. - -At this point, all we need to do is to change our *HTML* page to use *pyodide* instead: - -```html - - - - - - PyScript Offline - - - - - - interpreter = "/pyodide/pyodide.mjs" - - - - -``` - -## Wrapping it up - -We are basically done! If we try to disconnect from the internet but we still run our local server, the page will still show that very same *Hello from PyScript* message :partying_face: - -We can now drop internet, still keeping the local server running, and everything should be fine :partying_face: - -## Local Pyodide Packages - -There's one last thing that users are probably going to need: the ability to install Python packages when using Pyodide. - -In order to have also 3rd party packages available, we can use the bundle from [pyodide releases](https://github.com/pyodide/pyodide/releases/tag/0.24.1) that contains also packages. - -Please note this bundle is more than 200MB: it not downloaded all at once, it contains each package that is required and it loads only related packages when needed. - -Once downloaded and extracted, where in this case I am using `0.24.1` as reference bundle, we can literally copy and paste, or even move, all those files and folders inside the `pyodide-0.24.1/pyodide/*` directory into our `./public/pyodide/*` folder. - -As the bundle contains files already present, feel free to either skip or replace the content, or even directly move that *pyodide* folder inside our `./public/` one. - -Once it's done, we can now use any package we like that is available in *pyodide*. Let's see an example: - -```html - - - - - - PyScript Offline - - - - - - interpreter = "/pyodide/pyodide.mjs" - packages = ["pandas"] - - - - -``` - -If everything went fine, we should now be able to read `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` on the page *even* if we disconnect from the Internet. - -And **that's all folks** :wave: \ No newline at end of file diff --git a/docs/user-guide/terminal.md b/docs/user-guide/terminal.md index 374d04f..607f4da 100644 --- a/docs/user-guide/terminal.md +++ b/docs/user-guide/terminal.md @@ -1,40 +1,39 @@ # Terminal -In conventional (non-browser based) Python, it is common to run scripts from -the terminal, or to interact directly with the Python interpreter via the -[REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop). -It's to the terminal that `print` writes characters (via `stdout`), and it's -from the terminal that the `input` reads characters (via `stdin`). +Traditional Python development often happens in terminals where you run +scripts, interact with the REPL, and see output from `print()` +statements. PyScript brings this familiar environment to the browser +through a built-in terminal based on +[XTerm.js](https://xtermjs.org/). -It usually looks something like this: +This guide explains how to use PyScript's terminal for output, input, +and interactive Python sessions in your browser applications. - +## Basic terminal output -Because of the historic importance of the use of a terminal, PyScript makes one -available in the browser (based upon [XTerm.js](https://xtermjs.org/)). -As [mentioned earlier](first-steps.md), PyScript's built-in terminal is -activated with the `terminal` flag when using the ` + ``` -The end result will look like this (the rectangular box indicates the current -position of the cursor): +This creates a read-only terminal displaying your program's output. +Output appears in a terminal window on your page, familiar to anyone +who's used Python in a traditional environment. -Should you need an interactive terminal, for example because you use the -`input` statement that requires the user to type things into the terminal, you -**must ensure your code is run on a worker**: +The terminal captures standard output, so all `print()` statements write +to it automatically. This works with both Pyodide and MicroPython. + +## Interactive input + +To accept user input with the `input()` function, you must run your code +in a worker. Interactive terminals require workers to handle blocking +input without freezing the page: ```html ``` + +The `worker` attribute ensures input operations don't block the main +thread. Users can type into the terminal, and your code receives their +input through the familiar `input()` function. + -To use the interactive Python REPL in the terminal, use Python's -[code](https://docs.python.org/3/library/code.html) module like this: +Without the worker, interactive input would freeze your page. The worker +keeps the UI responsive whilst waiting for user input. + +## Interactive REPL + +For an interactive Python REPL session, use Python's `code` module: ```python import code @@ -53,145 +61,168 @@ import code code.interact() ``` -The end result should look something like this: +This starts a full Python REPL in the terminal where users can type +Python expressions and see results immediately: -Finally, it is possible to dynamically pass Python code into the terminal. The -trick is to get a reference to the terminal created by PyScript. Thankfully, -this is very easy. +The REPL provides command history, tab completion, and all the features +you'd expect from a traditional Python interactive session. This is +particularly useful for educational applications or debugging tools. + +## Programmatic control -Consider this fragment: +You can send code to the terminal programmatically from JavaScript. Give +your script an ID, then call the `process()` method: ```html - + + + ``` -Get a reference to the element, and just call the `process` method on -that object: +This lets you build interfaces where buttons or other controls trigger +Python execution in the terminal, useful for tutorials or interactive +demonstrations. -```JS -const myterm = document.querySelector("#my_script"); -await myterm.process('print("Hello world!")'); -``` +## Customising the terminal -## XTerm reference +Each terminal provides access to the underlying XTerm.js Terminal +instance through the `__terminal__` reference in Python or the +`terminal` property in JavaScript. -Each terminal has a reference to the -[Terminal](https://xtermjs.org/docs/api/terminal/classes/terminal/) -instance used to bootstrap the current terminal. +### Changing appearance -On the JavaScript side, it's a `script.terminal` property while on the Python -side, it's a `__terminal__` special reference that guarantees to provide the -very same `script.terminal`: +Customise terminal appearance through XTerm.js options: -```html title="How to reach the XTerm Terminal" - ``` -### Clear the terminal +This accesses XTerm.js's full configuration API, letting you adjust +colours, fonts, cursor styles, and other visual properties. -It's very simple to clear a PyTerminal: +### Resizing -```html title="Clearing the terminal" - +Control terminal dimensions programmatically: + +```python +if '__terminal__' in locals(): + __terminal__.resize(60, 10) # (width, height) ``` -### Resize the terminal +This adjusts terminal size dynamically, useful when building responsive +interfaces or compact terminal displays. -The terminal takes up a fair amount of room onscreen. It can be resized to use less. -Here it is 10 lines high. -```python title="Resizing the terminal in python" -if '__terminal__' in locals(): # has a terminal been created - __terminal__.resize(60, 10) # (width, height) +### Clearing output + +Clear the terminal programmatically: + +```html + ``` -### Terminal colors +Only output after the clear appears in the terminal. + +### Colours and formatting -Colors and most special characters work so you can make the text **bold** or -turn it green. You could even use a control -character to `print('\033[2J')` and clear the terminal, instead of using the -exposed `clear()` method: +The terminal supports ANSI escape codes for text formatting: -```html title="Terminal colors" +```html ``` -### Terminal addons +Use standard terminal control sequences for bold text, colours, and +other formatting. This works like traditional terminal applications. -It's possible [use XTerm.js addons](https://xtermjs.org/docs/guides/using-addons/): +## XTerm.js addons -```html title="Terminal addons" +Extend terminal functionality using +[XTerm.js addons](https://xtermjs.org/docs/guides/using-addons/): + +```html - [js_modules.main] - "https://cdn.jsdelivr.net/npm/@xterm/addon-web-links/+esm" = "weblinks" +[js_modules.main] +"https://cdn.jsdelivr.net/npm/@xterm/addon-web-links/+esm" = "weblinks" + ``` -By default we enable the `WebLinksAddon` addon (so URLs displayed in the -terminal automatically become links). Behind the scenes is the example code -shown above, and this approach will work for -[any other addon](https://github.com/xtermjs/xterm.js/tree/master/addons/) you -may wish to use. - -### MicroPython - -MicroPython has a -[very complete REPL](https://docs.micropython.org/en/latest/reference/repl.html) -already built into it. - - * All `Ctrl+X` strokes are handled, including paste mode and kill switches. - * History works out of the box. Access this via the up and down arrows to - view your command history. - * Tab completion works like a charm. Use the `tab` key to see available - variables or objects in `globals`. - * Copy and paste is much improved. This is true for a single terminal entry, - or a - [paste mode](https://docs.micropython.org/en/latest/reference/repl.html#paste-mode) - enabled variant. - -As a bonus, the MicroPython terminal works on both the main thread and in -web workers, with the following caveats: - -* **Main thread:** - * Calls to the blocking `input` function are delegated to the native browser - based - [prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) - utility. - * There are no guards against blocking code (e.g. `while True:` loops). - Such blocking code _could freeze your page_. -* **Web worker:** - * Conventional support for the `input` function, without blocking the main - thread. - * Blocking code (e.g. `while True:` loops) does not block the main thread - and your page will remain responsive. - -We encourage the usage of `worker` attribute to bootstrap a MicroPython -terminal. But now you have an option to run the terminal in the main thread. -Just remember not to block! +PyScript enables the WebLinksAddon by default, making URLs in terminal +output automatically clickable. You can load +[other addons](https://github.com/xtermjs/xterm.js/tree/master/addons/) +following the same pattern. + +## MicroPython REPL features + +MicroPython includes a comprehensive built-in REPL with several +convenience features: + +Command history works through up and down arrows, letting you recall and +edit previous commands. Tab completion shows available variables and +object attributes when you press tab. Copy and paste works naturally for +both single commands and +[paste mode](https://docs.micropython.org/en/latest/reference/repl.html#paste-mode) +for multi-line code. + +Control sequences like Ctrl+X work as expected, including paste mode +toggles and interrupt signals. + +## Worker versus main thread + +MicroPython terminals work in both environments with different +characteristics: + +**Main thread terminals** delegate `input()` to the browser's native +`prompt()` dialog. This works but feels less integrated. Blocking code +like infinite loops can freeze your page, so avoid long-running +operations on the main thread. + +**Worker terminals** provide proper `input()` support directly in the +terminal without blocking. Long-running code executes without freezing +the page. The UI stays responsive even during computation. + +We recommend using the `worker` attribute for MicroPython terminals +unless you have specific reasons to use the main thread. Workers provide +a better user experience and prevent blocking issues. + +## What's next + +Now that you understand terminals, explore these related topics: + +**[Editor](editor.md)** - Create interactive Python coding environments in +web pages with the built-in code editor. + +**[PyGame](pygame-ce.md)** - Use PyGame-CE with PyScript, covering the +differences from traditional PyGame development and techniques for making +games work well in the browser. + +**[Plugins](plugins.md)** - Understand the plugin system, lifecycle hooks, +and how to write plugins that integrate with PyScript. \ No newline at end of file diff --git a/docs/user-guide/what.md b/docs/user-guide/what.md index 84304c1..3e63560 100644 --- a/docs/user-guide/what.md +++ b/docs/user-guide/what.md @@ -1,34 +1,263 @@ # What is PyScript? [PyScript](https://pyscript.net) is an -[open source](../../license/) platform for -[Python](https://python.org) in the -[browser](https://en.wikipedia.org/wiki/Web_browser). +[open source](../../license/) platform for running +[Python](https://python.org) in modern +[web browsers](https://en.wikipedia.org/wiki/Web_browser). -PyScript brings together two of the most vibrant technical ecosystems on the -planet. If [the web](https://en.wikipedia.org/wiki/World_Wide_Web) and Python -had a baby, you'd get PyScript. +PyScript brings together two of the most vibrant technical ecosystems +on the planet: the web and Python. It lets you build rich, interactive +web applications using Python, without the need for a backend server or +entanglements with browser-based JavaScript (although PyScript also works +well in these two contexts). -PyScript works because modern browsers support -[WebAssembly](https://webassembly.org/) (abbreviated to WASM) - an +Write your application logic in Python, use Python libraries for data +processing or visualisation, and deploy your work with just a URL. Your +users run everything locally in their browser, making PyScript +applications fast, secure, and easy to share. + +## How it works + +PyScript is built on [WebAssembly](https://webassembly.org/) +(abbreviated to WASM) - an [instruction set](https://en.wikipedia.org/wiki/Instruction_set_architecture) -for a [virtual machine](https://en.wikipedia.org/wiki/Virtual_machine) with -an open specification and near native performance. PyScript takes -versions of the Python interpreter compiled to WASM, and makes them easy to use -inside the browser. - -At the core of PyScript is a _philosophy of digital empowerment_. The web is -the world's most ubiquitous computing platform, mature and familiar to billions -of people. Python is one of the +for a [virtual machine](https://en.wikipedia.org/wiki/Virtual_machine) +with an open specification and near-native performance. Modern browsers +all support WebAssembly, making it a universal platform for running code +beyond JavaScript. + +PyScript takes versions of the Python interpreter compiled to +WebAssembly and makes them easy to use inside the browser. You write +Python code, and PyScript handles all the complexity of loading the +interpreter, managing the environment, and bridging between Python and +the browser's JavaScript APIs. + +### Two Python interpreters + +PyScript supports two Python interpreters, letting you choose the right +tool for your application: + +Pyodide logo + +**[Pyodide](architecture.md#pyodide)** is the full CPython interpreter +compiled to WebAssembly. It's the standard Python you already know: the same +interpreter that runs on your laptop, with the same standard library and the same +behaviour. Because it's genuine CPython, Pyodide gives you access to +Python's vast ecosystem of packages from [PyPI](https://pypi.org/). Want +to use NumPy, Pandas, Matplotlib, Scikit-learn, or thousands of other +libraries? Pyodide makes it possible. + +MicroPython logo + +**[MicroPython](architecture.md#micropython)** is a lean, efficient +reimplementation of Python 3 that includes a comprehensive subset of the +standard library. At just 170KB, MicroPython loads almost instantly, +making it ideal for mobile devices, slow connections, or any time you +want your app to start quickly. Despite its small size, MicroPython is +surprisingly capable, exposing Python's full expressiveness to the +browser. + +Both interpreters implement almost identical foreign function interfaces (FFI) +to bridge Python and JavaScript, so your code works consistently +regardless of which interpreter you choose (and PyScript provides abstractions +around the differences between their FFIs). + +!!! tip + + **When to use which interpreter:** + + Choose **Pyodide** when you need access to Python's extensive + computing stack (NumPy, Pandas, Matplotlib, etc.), when you're + working with complex Python packages, or when you need full CPython + compatibility. + + Choose **MicroPython** for mobile applications, when fast startup + time matters, or when you're building lightweight applications that + don't need heavy libraries. + +### The foreign function interface + +The FFI (foreign function interface) is how Python and JavaScript +communicate in PyScript. It automatically translates between Python and +JavaScript objects, letting you use browser APIs directly from your +Python code. + +This bridge is bidirectional: Python can call JavaScript functions and +access JavaScript objects, while JavaScript can call Python functions +and access Python objects. The FFI handles all the type conversions +automatically, so you can focus on writing your application logic. + +Want to manipulate the DOM? Access the browser's `document` object. +Need to use a JavaScript library? Import it and call its functions from +Python. The FFI makes it seamless. + +Put simply, PyScript helps Python and JavaScript to be friends that +complement and amplify each others strengths. + +Learn more about the FFI in the high level [DOM interaction guide](dom.md#ffi) +or dive deep into the [FFI's technical details](ffi.md). + +## Key capabilities + +PyScript provides a rich set of features that make building web +applications with Python both powerful and enjoyable: + +### Full web platform access + +PyScript gives you complete access to the [DOM](dom.md) and all the +[web APIs](https://developer.mozilla.org/en-US/docs/Web/API) +implemented by your browser. Through the FFI, Python works seamlessly +with everything the browser offers, including any third-party JavaScript +libraries included in your page. + +The `pyscript.web` module provides a Pythonic interface to the DOM, +making it feel natural to work with web page elements from Python. Find +elements with CSS selectors, manipulate content and attributes, handle +events - all with idiomatic Python code. + +### Python's vast ecosystem + +Because Pyodide is genuine CPython compiled to WebAssembly, you have +access to Python's deep and diverse ecosystem of libraries, frameworks, +and modules. Whether you're doing data science, building visualisations, +processing text, or working with APIs, there's probably a Python library +to help. + +Got a favourite library in Python? Now you can use it in the browser and +share your work with just a URL. No server required, no complex +deployment - just Python running where your users are. + +Need to check if a package is supported by PyScript? Use our +[PyScript Packages](https://pyscript.github.io/pyscript-packages/) website +to check and/or report the status of any third party packages. + +### AI and data science built in + +Python is famous for its extraordinary usefulness in artificial +intelligence and data science. The Pyodide interpreter comes with many +of the libraries needed for this sort of work already included: NumPy, +Pandas, Matplotlib, Scikit-learn, and more. + +Build interactive data visualisations, create machine learning +demonstrations, or develop educational tools that let users experiment +with algorithms - all running locally in the browser with no backend +required! + +### Mobile-friendly MicroPython + +Thanks to MicroPython's tiny size and fast startup, PyScript +provides a compelling story for Python on mobile devices. Your +application loads quickly on first visit and almost instantly on +subsequent visits (thanks to browser caching). + +This makes PyScript practical for mobile web applications, progressive +web apps, or any scenario where fast initial load matters. + +### Background processing with workers + +Expensive computation can block the main thread, making your application +appear frozen. PyScript supports +[web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API), +letting you run Python code in background threads that don't interfere +with the user interface. + +Think of workers as independent subprocesses in your web page. They're +perfect for data processing, simulations, or any CPU-intensive work. +Learn more in the [workers guide](workers.md). + +### Device capabilities + +PyScript gives your Python code access to modern browser capabilities: +capture photos and video with the [camera](media.md#camera), record +[audio](media.md#audio), read and write [files](filesystem.md), store +data [locally](storage.md), and integrate with all the other available +web APIs. + +Build applications that feel native, with access to device hardware and +local storage, all from Python. + +### Extensible plugin system + +PyScript has a small, efficient core called +[PolyScript](https://github.com/pyscript/polyscript). Most of +PyScript's functionality is actually implemented through PolyScript's +[plugin system](plugins.md). + +This architecture ensures a clear separation of concerns: PolyScript +focuses on being small, efficient, and powerful, whilst the PyScript +plugins build Pythonically upon these solid foundations. + +The plugin system also means developers independent of the PyScript core +team can create and contribute plugins whose functionality reflects the +unique and diverse needs of PyScript's users. + +## Our aim: digital empowerment + +At the core of PyScript is a philosophy of digital empowerment. + +The web is the world's most ubiquitous computing platform, mature and +familiar to billions of people. Python is one of the [world's most popular programming languages](https://spectrum.ieee.org/the-top-programming-languages-2023): -it is easy to teach and learn, used in a plethora of existing domains -(such as data science, games, embedded systems, artificial intelligence, -finance, physics and film production - to name but a few), and the Python -ecosystem contains a huge number of popular and powerful libraries to address -its many uses. +it's easy to teach and learn, used across countless domains (data +science, education, games, embedded systems, artificial intelligence, +finance, physics, film production - to name but a few), and the Python +ecosystem contains a vast number of popular and powerful libraries. + +PyScript brings together the ubiquity, familiarity, and accessibility of +the web with the power, depth, and expressiveness of Python. + +This means PyScript isn't just for programming experts but, as we like +to say, **for the 99% of the rest of the planet who use computers**. + +By making Python accessible in the browser, PyScript lowers barriers to +creating and sharing software. You don't need to understand server +deployment, database management, or complex build processes. You don't +need to learn a new language or framework. You just need Python and a +web browser. + +This democratisation of web development means more people can create +tools to solve their own problems, share their knowledge with others, +and contribute to the digital world we all inhabit. + +## What you can build + +PyScript opens up new possibilities for Python developers and new +capabilities for web applications: + +**Educational tools and interactive tutorials** - Create lessons where +students can experiment with code directly in their browser, see +visualisations update in real-time, and learn by doing. + +**Data analysis dashboards** - Build interactive visualisations of your +data using Pandas and Matplotlib, then share them with colleagues who +can explore the data themselves without installing anything. + +**Scientific simulations** - Develop models and simulations that run +entirely in the browser, letting others experiment with parameters and +see results instantly. + +**Creative coding projects** - Make generative art, music +visualisations, or interactive games using Python libraries you already +know. + +**Rapid prototypes** - Test ideas quickly without setting up backend +infrastructure. Share prototypes with a simple URL. + +**Browser-based tools** - Create utilities that process files, transform +data, or automate tasks - all running locally for privacy and speed. + +The only limit is your imagination (and perhaps your users' patience +whilst the interpreter loads, but that's getting faster all the time). + +## Next steps + +Ready to start building with PyScript? -PyScript brings together the ubiquity, familiarity and accessibility of the web -with the power, depth and expressiveness of Python. It means PyScript isn't -just for programming experts but, as we like to say, for the 99% of the rest of -the planet who use computers. +Explore [interacting with the web page](dom.md) to learn how a +PyScript application interacts with the browser, or jump to +[Configuration](configuration.md) to understand how to set up your Python +environment. +Want to see PyScript in action first? Check out the +[example applications](../example-apps/overview.md) to see what's +possible and learn from working code. \ No newline at end of file diff --git a/docs/user-guide/workers.md b/docs/user-guide/workers.md index 970e024..d434b87 100644 --- a/docs/user-guide/workers.md +++ b/docs/user-guide/workers.md @@ -1,351 +1,275 @@ # Workers -Workers run code that won't block the "main thread" controlling the user -interface. If you block the main thread, your web page becomes annoyingly -unresponsive. **You should never block the main thread.** +Workers run Python code in background threads, keeping your user interface +responsive. Without workers, long computations block the main thread and freeze +the page. With workers, heavy tasks run in the background whilst the UI stays +smooth and interactive. -Happily, PyScript makes it very easy to use workers and uses a feature recently -added to web standards called -[Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics). -**You don't need to know about Atomics to use web workers**, but it's useful to -know that the underlying [coincident library](architecture.md#coincident) -uses it under the hood. +This guide explains how to use workers in PyScript, when to use them, and how +to structure your applications to take advantage of background processing. -!!! info +## Understanding the problem - Sometimes you only need to `await` in the main thread on a method in a - worker when neither `window` nor `document` are referenced in the code - running on the worker. +JavaScript (and therefore PyScript) runs on a single "main" thread. When Python +code executes, nothing else can happen. Long computations freeze the interface. +Users cannot click buttons, scroll, or interact with the page until the +computation completes. - In these cases, you don't need any special header or service worker - as long as **the method exposed from the worker returns a serializable - result**. +Workers solve this problem by running Python in separate threads. The main +thread handles the UI. Workers handle computation. Both run simultaneously, so +your application remains responsive even during heavy processing. -## HTTP headers +## Defining workers -To use the `window` and `document` objects from within a worker (i.e. use -synchronous Atomics) **you must ensure your web server enables the following -headers** (this is the default behavior for -[pyscript.com](https://pyscript.com)): +Workers are defined with ` +``` -The simplest way to use mini-coi is to copy the -[mini-coi.js](https://raw.githubusercontent.com/WebReflection/mini-coi/main/mini-coi.js) -file content and save it in the root of your website (i.e. `/`), and reference -it as the first child tag in the `` of your HTML documents: +The `worker` attribute marks the script as a worker. The `name` attribute +provides a unique identifier for accessing the worker from other code. The +`type` attribute specifies the interpreter: `py` for Pyodide or `mpy` for +MicroPython. -```html - - - - - - - -``` +Workers must explicitly export functions using the `__export__` list. Only +exported functions are accessible from the main thread. This keeps the API +clear and prevents accidental exposure of internal implementation details. -### Option 2: `service-worker` attribute +## Accessing workers -This allows you to slot in a custom -[service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) -to handle requirements for synchronous operations. +Access workers from the main thread using the `workers` object: -Each ` - - +# Call its exported function. +result = await calc.add_arrays([1, 2, 3], [4, 5, 6]) +print(result) # [5, 7, 9] ``` -!!! warning - - Using *coincident* as the fallback for synchronous operations via Atomics - should be **the last solution to consider**. It is inevitably - slower than using native Atomics. +Worker access is asynchronous because workers may not be ready immediately. +They need time to download and initialise the interpreter, load configured +packages, execute the worker script, and register exported functions. The +`await` ensures the worker is fully ready before you use it. - If you must use `service-worker` attribute, always reduce the amount of - synchronous operations by caching references from the *main* thread. +Once you have a worker reference, call its exported functions like normal async +functions. **All calls must be awaited, and all data passed between the main +thread and workers must be serialisable (numbers, strings, lists, dictionaries, +booleans, None)**. You cannot pass functions, classes, or complex objects. - ```python - # ❌ THIS IS UNNECESSARILY SLOWER - from pyscript import document +## Choosing interpreters - # add a data-test="not ideal attribute" - document.body.dataset.test = "not ideal" - # read a data-test attribute - print(document.body.dataset.test) +PyScript supports two Python interpreters, and you can mix them based on your +needs. The main thread and workers can use different interpreters. - # - - - - - - - - - - - - - - - - - - - - - +Pyodide (`type="py"`) provides full CPython compatibility with access to +weighty packages like numpy and pandas. It has a larger download size and +slower startup, but offers the complete Python ecosystem. Use Pyodide for heavy +computation requiring numerical libraries, tasks needing the full Python +ecosystem, or complex data processing. - # ✔️ THIS IS FINE - from pyscript import document +MicroPython (`type="mpy"`) provides fast startup with a small footprint. It +includes core Python only, with no pip packages. Use MicroPython for +lightweight tasks, quick worker startup, or simple computations when you don't +need packages. - # if needed elsewhere, reach it once - body = document.body - dataset = body.dataset +A common pattern is MicroPython on the main thread for a fast, responsive UI, +with Pyodide in workers for powerful computation when needed: - # add a data-test="not ideal attribute" - dataset.test = "not ideal" - # read a data-test attribute - print(dataset.test) - ``` +```html + + -In latter example the number of operations has been reduced from six to just -four. The rule of thumb is: _if you ever need a DOM reference more than once, -cache it_. 👍 + + +__export__ = ["crunch_numbers"] + ``` -You may also want to add a `name` attribute to the tag, so you can use -`pyscript.workers` in the main thread to retrieve a reference to the worker: +## Creating workers dynamically -```html - -``` +Create workers from Python code using `create_named_worker()`: ```python -from pyscript import workers +from pyscript import create_named_worker + -my_worker = await workers["my-worker"] +# Create a Pyodide worker. +worker = await create_named_worker( + src="./background_tasks.py", + name="task-processor", + config={"packages": ["pandas"]} +) + +# Use it immediately. +result = await worker.process_data() ``` -Alternatively, to launch a worker from within Python running on the main thread -use the [pyscript.PyWorker](../../api/#pyscriptpyworker) class and you must -reference both the target Python script and interpreter type: +This is useful for spawning workers based on user actions, creating multiple +workers for parallel processing, or loading workers conditionally based on +application state. -```python title="Launch a worker from within Python" -from pyscript import PyWorker +The function accepts four parameters. The `src` parameter specifies the path to +the worker's Python file. The `name` parameter provides a unique identifier for +the worker. The `config` parameter accepts a configuration dictionary or JSON +string (optional). The `type` parameter specifies the interpreter: `"py"` +(default) or `"mpy"` (optional). -# The type MUST be given and can be either `micropython` or `pyodide` -my_worker = PyWorker("my-worker-code.py", type="micropython") -``` +## Configuration + +Workers support the same configuration as main thread scripts. You can specify +packages to install, files to fetch, and JavaScript modules to import. See the +[Configuration guide](configuration.md) for complete details on available +options. + +The configuration is provided either inline as a JSON string in the `config` +attribute, or as a path to a configuration file: -## Worker interactions +```html + + + + + +``` -Code running in the worker needs to be able to interact with code running in -the main thread and perhaps have access to the web page. This is achieved via -some helpful [builtin APIs](../../api). +## Example: Prime number calculator -!!! note +Here's a complete example demonstrating workers in action: - For ease of use, the worker related functionality in PyScript is - a simpler presentation of more sophisticated and powerful behaviour - available via PolyScript. + - **If you are a confident advanced user**, please - [consult the XWorker](https://pyscript.github.io/polyscript/#xworker) - related documentation from the PolyScript project for how to make use of - these features. +[View the complete source code](https://github.com/pyscript/docs/tree/main/docs/example-apps/prime-worker). -To synchronise serializable data between the worker and the main thread use -[the `sync` function](../../api/#pyscriptsync) in the worker to reference a -function registered on the main thread: +This application finds prime numbers using the +[Sieve of Eratosthenes algorithm](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes). +The heavy computation runs in a worker, keeping the main thread responsive. -```python title="Python code running on the main thread." -from pyscript import PyWorker +The HTML page defines two scripts. The main thread uses MicroPython and +handles the UI: -def hello(name="world"): - return(f"Hello, {name}") +```html + +``` -# Create the worker. -worker = PyWorker("./worker.py", type="micropython") +The worker uses Pyodide with numpy and does the computation: -# Register the hello function as callable from the worker. -worker.sync.hello = hello +```html + +__export__ = ["find_primes"] ``` -```python title="Referencing and using the worker from the main thread." -from pyscript import workers +Watch the pulsing green indicator whilst computing. It never stops, +proving the main thread stays responsive. Try entering different values +to see the worker handle various workloads. The "Use NumPy" checkbox +lets you compare the performance of numpy's array operations against +pure Python - a nice demonstration of why numerical libraries matter for +computational tasks. -my_worker = await workers["my-worker"] +## Understanding limitations -print(await my_worker.version()) -``` +Workers have separate memory spaces. Each worker has its own memory, and +you cannot share objects between workers or with the main thread. All +communication happens via function calls with serialised data. -The values passed between the main thread and the worker **must be -serializable**. Try the example given above via -[this project on PyScript.com](https://pyscript.com/@ntoll/tiny-silence/latest). - -No matter if your code is running on the main thread or in a web worker, -both the [`pyscript.window`](../../api/#pyscriptwindow) (representing the main -thread's global window context) and -[`pyscript.document`](../../api/#pyscriptdocument) (representing the web -page's -[document object](https://developer.mozilla.org/en-US/docs/Web/API/Document)) -will be available and work in the same way. As a result, a worker can reach -into the DOM and access some `window` based APIs. - -!!! warning - - Access to the `window` and `document` objects is a powerful feature. Please - remember that: - - * Arguments to and the results from such calls, when used in a worker, - **must be serializable**, otherwise they won't work. - * If you manipulate the DOM via the `document` object, and other workers or - code on the main thread does so too, **they may interfere with each other - and produce unforeseen problematic results**. Remember, with great power - comes great responsibility... and we've given you a bazooka (so please - remember not to shoot yourself in the foot with it). - -## Common Use Case - -While it is possible to start a MicroPython or Pyodide worker from either -MicroPython or Pyodide running on the main thread, the most common use case -we have encountered is MicroPython on the main thread starting a Pyodide -worker. - -Here's how: - -**index.html** -```HTML title="Evaluate main.py via MicroPython on the main thread" - - - - - - - - - - PyWorker - mpy bootstrapping pyodide example - - - -``` +Only serialisable data can pass between threads. Function arguments and +return values must be JSON-serialisable: numbers, strings, lists, +dictionaries, booleans, and None work. Functions, classes, file handles, +and numpy arrays (convert to lists first) do not work. -**main.py** -```Python title="MicroPython's main.py: bootstrapping a Pyodide worker." -from pyscript import PyWorker, document - -# Bootstrap the Pyodide worker, with optional config too. -# The worker is: -# * Owned by this script, no JS or Pyodide code in the same page can access -# it. -# * It allows pre-sync methods to be exposed. -# * It has a ready Promise to await for when Pyodide is ready in the worker. -# * It allows the use of post-sync (methods exposed by Pyodide in the -# worker). -worker = PyWorker("worker.py", type="pyodide") - -# Expose a utility that can be immediately invoked in the worker. -worker.sync.greetings = lambda: print("Pyodide bootstrapped") - -print("before ready") -# Await until Pyodide has completed its bootstrap, and is ready. -await worker.ready -print("after ready") - -# Await any exposed methods exposed via Pyodide in the worker. -result = await worker.sync.heavy_computation() -print(result) - -# Show the result at the end of the body. -document.body.append(result) - -# Free memory and get rid of everything in the worker. -worker.terminate() -``` +Workers need time to initialise. Pyodide workers especially may take time +to download packages and start up. The first call may be slow. Plan your +application accordingly and consider showing loading indicators during +initialisation. -**worker.py** -```Python title="The worker.py script runs in the Pyodide worker." -from pyscript import sync +User activation requirements apply. Creating workers dynamically with +`create_named_worker()` during page load works fine. However, if your +worker needs to access certain browser features, those features may +require user activation (a button click or similar interaction). -# Use any methods from main.py on the main thread. -sync.greetings() +## What's next -# Expose any methods meant to be used from main. -sync.heavy_computation = lambda: 6 * 7 -``` +Now that you understand workers, explore these related topics to deepen +your knowledge. -Save these files in a `tmp` folder, ensure [your headers](#http-headers) (just -use `npx mini-coi ./tmp` to serve via localhost) then see the following -outcome in the browser's devtools. +**[Architecture guide](architecture.md)** - provides technical details about +how PyScript implements workers using PolyScript and Coincident if you're +interested in the underlying mechanisms. -``` -before ready -Pyodide bootstrapped -after ready -42 -``` +**[Filesystem](filesystem.md)** - Learn more about the virtual +filesystem and how the `files` option works. + +**[FFI](ffi.md)** - Understand how JavaScript modules integrate with +Python through the foreign function interface. + +**[Media](media.md)** - Capture photos and video with the camera or +record audio with your microphone. + +**[Offline](offline.md)** - Use PyScript while not connected to the internet. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 280b6a3..ca79cda 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,25 +73,38 @@ plugins: nav: - Home: index.md - - Beginning PyScript: beginning-pyscript.md - - Example Applications: examples.md + - Start here: beginning-pyscript.md - User guide: - Introduction: user-guide/index.md - What is PyScript?: user-guide/what.md - - Features: user-guide/features.md - - First steps: user-guide/first-steps.md - - Architecture: user-guide/architecture.md - - Configure PyScript: user-guide/configuration.md - - The DOM & JavaScript: user-guide/dom.md - - Web Workers: user-guide/workers.md - - The FFI in detail: user-guide/ffi.md - - PyScript and filesystems: user-guide/filesystem.md - - Python terminal: user-guide/terminal.md - - Python editor: user-guide/editor.md - - Media: user-guide/media.md - - PyGame-CE: user-guide/pygame-ce.md - - Plugins: user-guide/plugins.md - - Use Offline: user-guide/offline.md + - Core concepts: + - The DOM & JavaScript: user-guide/dom.md + - Events: user-guide/events.md + - Displaying things: user-guide/display.md + - Configure PyScript: user-guide/configuration.md + - Advanced topics: + - Architecture: user-guide/architecture.md + - Web Workers: user-guide/workers.md + - PyScript and filesystems: user-guide/filesystem.md + - Media: user-guide/media.md + - The FFI in detail: user-guide/ffi.md + - Use Offline: user-guide/offline.md + - Feature guides: + - Python terminal: user-guide/terminal.md + - Python editor: user-guide/editor.md + - PyGame-CE: user-guide/pygame-ce.md + - Plugins: user-guide/plugins.md + - Example Applications: + - Overview: example-apps/overview.md + - Bouncing Ball: example-apps/bouncing-ball/info.md + - Colour Picker: example-apps/colour-picker/info.md + - Display Demos: example-apps/display-demo/info.md + - Note Taker: example-apps/note-taker/info.md + - Photobooth: example-apps/photobooth/info.md + - Pirate Translator: example-apps/pirate-translator/info.md + - Prime Number Finder: example-apps/prime-worker/info.md + - Task Board (FFI): example-apps/task-board-ffi/info.md + - Task Board (Pythonic): example-apps/task-board-web/info.md - PyScript APIs: - Introduction: api/init.md - context: api/context.md @@ -107,8 +120,9 @@ nav: - web: api/web.md - websocket: api/websocket.md - workers: api/workers.md + - Developing PyScript: + - Contributing: contributing.md + - Developer Guide: developers.md + - Code of Conduct: conduct.md + - License: license.md - FAQ: faq.md - - Contributing: contributing.md - - Developer Guide: developers.md - - Code of Conduct: conduct.md - - License: license.md From d6bf8608fb0b773572c3c8a6dcf652770d3e845d Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 8 Jan 2026 15:51:09 +0000 Subject: [PATCH 10/10] rebase --- docs/beginning-pyscript.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/beginning-pyscript.md b/docs/beginning-pyscript.md index 7419448..3b850e2 100644 --- a/docs/beginning-pyscript.md +++ b/docs/beginning-pyscript.md @@ -240,11 +240,7 @@ 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`