From 13c566ce033049712b85a2feb570bdce963c0433 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Dec 2025 15:24:19 +0000 Subject: [PATCH 1/8] 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 7aa230e97883f57628d6aa088d2f032955edbe86 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Dec 2025 15:35:49 +0000 Subject: [PATCH 2/8] 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 dd56141839465ec3ad935da25d0dcdcf4cc80bf5 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Dec 2025 16:26:58 +0000 Subject: [PATCH 3/8] 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 3822ff20fbcd4d28ddb5d4bf6e1ba243ecfac54b Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Dec 2025 16:27:36 +0000 Subject: [PATCH 4/8] 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 d2d581cc0b4e60ac4d54fecbcf68c84b45d734c7 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Dec 2025 16:29:15 +0000 Subject: [PATCH 5/8] 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 583888656202cc7e36440bd9c543809f6bd50f31 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Mon, 8 Dec 2025 17:12:55 +0000 Subject: [PATCH 6/8] 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 `