diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 21327b0..0000000 --- a/docs/api.md +++ /dev/null @@ -1,1287 +0,0 @@ -# Built-in APIs - -PyScript makes available convenience objects, functions and attributes. - -In Python this is done via the builtin `pyscript` module: - -```python title="Accessing the document object via the pyscript module" -from pyscript import document -``` - -In HTML this is done via `py-*` and `mpy-*` attributes (depending on the -interpreter you're using): - -```html title="An example of a py-click handler" - -``` - -These APIs will work with both Pyodide and Micropython in exactly the same way. - -!!! info - - Both Pyodide and MicroPython provide access to two further lower-level - APIs: - - * Access to - [JavaScript's `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) - via importing the `js` module: `import js` (now `js` is a proxy for - `globalThis` in which all native JavaScript based browser APIs are - found). - * Access to interpreter specific versions of utilities and the foreign - function interface. Since these are different for each interpreter, and - beyond the scope of PyScript's own documentation, please check each - project's documentation - ([Pyodide](https://pyodide.org/en/stable/usage/api-reference.html) / - [MicroPython](https://docs.micropython.org/en/latest/)) for details of - these lower-level APIs. - -PyScript can run in two contexts: the main browser thread, or on a web worker. -The following three categories of API functionality explain features that are -common for both main thread and worker, main thread only, and worker only. Most -features work in both contexts in exactly the same manner, but please be aware -that some are specific to either the main thread or a worker context. - -## Common features - -These Python objects / functions are available in both the main thread and in -code running on a web worker: - -### `pyscript.config` - -A Python dictionary representing the configuration for the interpreter. - -```python title="Reading the current configuration." -from pyscript import config - - -# It's just a dict. -print(config.get("files")) -# This will be either "mpy" or "py" depending on the current interpreter. -print(config["type"]) -``` - -!!! info - - The `config` object will always include a `type` attribute set to either - `mpy` or `py`, to indicate which version of Python your code is currently - running in. - -!!! warning - - Changing the `config` dictionary at runtime has no effect on the actual - configuration. - - It's just a convenience to **read the configuration** at run time. - -### `pyscript.current_target` - -A utility function to retrieve the unique identifier of the element used -to display content. If the element is not a ` -``` - -!!! Note - - The return value of `current_target()` always references a visible element - on the page, **not** at the current ` - ``` - - Then use the standard `document.getElementById(script_id)` function to - return a reference to it in your code. - -### `pyscript.display` - -A function used to display content. The function is intelligent enough to -introspect the object[s] it is passed and work out how to correctly display the -object[s] in the web page based on the following mime types: - -* `text/plain` to show the content as text -* `text/html` to show the content as *HTML* -* `image/png` to show the content as `` -* `image/jpeg` to show the content as `` -* `image/svg+xml` to show the content as `` -* `application/json` to show the content as *JSON* -* `application/javascript` to put the content in `

- - - -

-``` - -### `pyscript.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. diff --git a/docs/api/context.md b/docs/api/context.md new file mode 100644 index 0000000..635214e --- /dev/null +++ b/docs/api/context.md @@ -0,0 +1,3 @@ +# `pyscript.context` + +::: pyscript.context diff --git a/docs/api/display.md b/docs/api/display.md new file mode 100644 index 0000000..fb98686 --- /dev/null +++ b/docs/api/display.md @@ -0,0 +1,3 @@ +# `pyscript.display` + +::: pyscript.display diff --git a/docs/api/events.md b/docs/api/events.md new file mode 100644 index 0000000..061c4ae --- /dev/null +++ b/docs/api/events.md @@ -0,0 +1,3 @@ +# `pyscript.event` + +::: pyscript.events diff --git a/docs/api/fetch.md b/docs/api/fetch.md new file mode 100644 index 0000000..c57937f --- /dev/null +++ b/docs/api/fetch.md @@ -0,0 +1,3 @@ +# `pyscript.fetch` + +::: pyscript.fetch diff --git a/docs/api/ffi.md b/docs/api/ffi.md new file mode 100644 index 0000000..69b5472 --- /dev/null +++ b/docs/api/ffi.md @@ -0,0 +1,3 @@ +# `pyscript.ffi` + +::: pyscript.ffi diff --git a/docs/api/flatted.md b/docs/api/flatted.md new file mode 100644 index 0000000..e8052ff --- /dev/null +++ b/docs/api/flatted.md @@ -0,0 +1,3 @@ +# `pyscript.flatted` + +::: pyscript.flatted diff --git a/docs/api/fs.md b/docs/api/fs.md new file mode 100644 index 0000000..6ef3363 --- /dev/null +++ b/docs/api/fs.md @@ -0,0 +1,3 @@ +# `pyscript.fs` + +::: pyscript.fs diff --git a/docs/api/init.md b/docs/api/init.md new file mode 100644 index 0000000..2da87b0 --- /dev/null +++ b/docs/api/init.md @@ -0,0 +1,14 @@ +# The `pyscript` API + +!!! important + + These API docs are auto-generated from our source code. To suggest + changes or report errors, please do so via + [our GitHub repository](https://github.com/pyscript/pyscript). The + source code for these APIs + [is found here](https://github.com/pyscript/pyscript/tree/main/core/src/stdlib/pyscript) + in our repository. + +::: pyscript + options: + show_root_heading: false diff --git a/docs/api/media.md b/docs/api/media.md new file mode 100644 index 0000000..1b19777 --- /dev/null +++ b/docs/api/media.md @@ -0,0 +1,3 @@ +# `pyscript.media` + +::: pyscript.media diff --git a/docs/api/storage.md b/docs/api/storage.md new file mode 100644 index 0000000..053df3c --- /dev/null +++ b/docs/api/storage.md @@ -0,0 +1,3 @@ +# `pyscript.storage` + +::: pyscript.storage diff --git a/docs/api/util.md b/docs/api/util.md new file mode 100644 index 0000000..d7e7db4 --- /dev/null +++ b/docs/api/util.md @@ -0,0 +1,3 @@ +# `pyscript.util` + +::: pyscript.util diff --git a/docs/api/web.md b/docs/api/web.md new file mode 100644 index 0000000..b1a3c1c --- /dev/null +++ b/docs/api/web.md @@ -0,0 +1,18 @@ +# `pyscript.web` + +::: pyscript.web + options: + members: + - page + - Element + - ContainerElement + - ElementCollection + - Classes + - Style + - HasOptions + - Options + - Page + - canvas + - video + - CONTAINER_TAGS + - VOID_TAGS diff --git a/docs/api/websocket.md b/docs/api/websocket.md new file mode 100644 index 0000000..093fda8 --- /dev/null +++ b/docs/api/websocket.md @@ -0,0 +1,3 @@ +# `pyscript.websocket` + +::: pyscript.websocket diff --git a/docs/api/workers.md b/docs/api/workers.md new file mode 100644 index 0000000..2930a8d --- /dev/null +++ b/docs/api/workers.md @@ -0,0 +1,3 @@ +# `pyscript.workers` + +::: pyscript.workers diff --git a/mkdocs.yml b/mkdocs.yml index d62e421..280b6a3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,6 @@ site_name: PyScript +site_author: The PyScript OSS Team +site_description: PyScript - an open source platform for Python in the browser. theme: name: material @@ -58,6 +60,16 @@ plugins: css_dir: css javascript_dir: js canonical_version: null + - mkdocstrings: + default_handler: python + locale: en + handlers: + python: + options: + show_source: true + members_order: source + show_symbol_type_heading: true + show_symbol_type_toc: true nav: - Home: index.md @@ -80,7 +92,21 @@ nav: - PyGame-CE: user-guide/pygame-ce.md - Plugins: user-guide/plugins.md - Use Offline: user-guide/offline.md - - Built-in APIs: api.md + - PyScript APIs: + - Introduction: api/init.md + - context: api/context.md + - display: api/display.md + - events: api/events.md + - fetch: api/fetch.md + - ffi: api/ffi.md + - flatted: api/flatted.md + - fs: api/fs.md + - media: api/media.md + - storage: api/storage.md + - util: api/util.md + - web: api/web.md + - websocket: api/websocket.md + - workers: api/workers.md - FAQ: faq.md - Contributing: contributing.md - Developer Guide: developers.md diff --git a/pyscript/__init__.py b/pyscript/__init__.py new file mode 100644 index 0000000..644ec56 --- /dev/null +++ b/pyscript/__init__.py @@ -0,0 +1,120 @@ +""" +This is the main `pyscript` namespace. It provides the primary Pythonic API +for users to interact with the +[browser's own API](https://developer.mozilla.org/en-US/docs/Web/API). It +includes utilities for common activities such as displaying content, handling +events, fetching resources, managing local storage, and coordinating with +web workers. + +The most important names provided by this namespace can be directly imported +from `pyscript`, for example: + +```python +from pyscript import display, HTML, fetch, when, storage, WebSocket +``` + +The following names are available in the `pyscript` namespace: + +- `RUNNING_IN_WORKER`: Boolean indicating if the code is running in a Web + Worker. +- `PyWorker`: Class for creating Web Workers running Python code. +- `config`: Configuration object for pyscript settings. +- `current_target`: The element in the DOM that is the current target for + output. +- `document`: The standard `document` object, proxied in workers. +- `window`: The standard `window` object, proxied in workers. +- `js_import`: Function to dynamically import JS modules. +- `js_modules`: Object containing JS modules available to Python. +- `sync`: Utility for synchronizing between worker and main thread. +- `display`: Function to render Python objects in the web page. +- `HTML`: Helper class to create HTML content for display. +- `fetch`: Function to perform HTTP requests. +- `Storage`: Class representing browser storage (local/session). +- `storage`: Object to interact with browser's local storage. +- `WebSocket`: Class to create and manage WebSocket connections. +- `when`: Function to register event handlers on DOM elements. +- `Event`: Class representing user defined or DOM events. +- `py_import`: Function to lazily import Pyodide related Python modules. + +If running in the main thread, the following additional names are available: + +- `create_named_worker`: Function to create a named Web Worker. +- `workers`: Object to manage and interact with existing Web Workers. + +All of these names are defined in the various submodules of `pyscript` and +are imported and re-exported here for convenience. Please refer to the +respective submodule documentation for more details on each component. + + +!!! Note + Some notes about the naming conventions and the relationship between + various similar-but-different names found within this code base. + + ```python + import pyscript + ``` + + The `pyscript` package contains the main user-facing API offered by + PyScript. All the names which are supposed be used by end users should + be made available in `pyscript/__init__.py` (i.e., this source file). + + ```python + import _pyscript + ``` + + The `_pyscript` module is an internal API implemented in JS. **End users + should not use it directly**. For its implementation, grep for + `interpreter.registerJsModule("_pyscript",...)` in `core.js`. + + ```python + import js + ``` + + The `js` object is + [the JS `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis), + as exported by Pyodide and/or Micropython's foreign function interface + (FFI). As such, it contains different things in the main thread or in a + worker, as defined by web standards. + + ```python + import pyscript.context + ``` + + The `context` submodule abstracts away some of the differences between + the main thread and a worker. Its most important features are made + available in the root `pyscript` namespace. All other functionality is + mostly for internal PyScript use or advanced users. In particular, it + defines `window` and `document` in such a way that these names work in + both cases: in the main thread, they are the "real" objects, in a worker + they are proxies which work thanks to + [coincident](https://github.com/WebReflection/coincident). + + ```python + from pyscript import window, document + ``` + + These are just the `window` and `document` objects as defined by + `pyscript.context`. This is the blessed way to access them from `pyscript`, + as it works transparently in both the main thread and worker cases. +""" + +from polyscript import lazy_py_modules as py_import +from pyscript.context import ( + RUNNING_IN_WORKER, + PyWorker, + config, + current_target, + document, + js_import, + js_modules, + sync, + window, +) +from pyscript.display import HTML, display +from pyscript.fetch import fetch +from pyscript.storage import Storage, storage +from pyscript.websocket import WebSocket +from pyscript.events import when, Event + +if not RUNNING_IN_WORKER: + from pyscript.workers import create_named_worker, workers diff --git a/pyscript/context.py b/pyscript/context.py new file mode 100644 index 0000000..b3f775c --- /dev/null +++ b/pyscript/context.py @@ -0,0 +1,198 @@ +""" +Execution context management for PyScript. + +This module handles the differences between running in the +[main browser thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread) +versus running in a +[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), +providing a consistent API regardless of the execution context. + +Key features: + +- Detects whether code is running in a worker or main thread. Read this via + the boolean `pyscript.context.RUNNING_IN_WORKER`. +- Parses and normalizes configuration from `polyscript.config` and adds the + Python interpreter type via the `type` key in `pyscript.context.config`. +- Provides appropriate implementations of `window`, `document`, and `sync`. +- Sets up JavaScript module import system, including a lazy `js_import` + function. +- Manages `PyWorker` creation. +- Provides access to the current display target via + `pyscript.context.display_target`. + +!!! warning + + These are key differences between the main thread and worker contexts: + + Main thread context: + + - `window` and `document` are available directly. + - `PyWorker` can be created to spawn worker threads. + - `sync` is not available (raises `NotSupported`). + + Worker context: + + - `window` and `document` are proxied from main thread (if SharedArrayBuffer + available). + - `PyWorker` is not available (raises `NotSupported`). + - `sync` utilities are available for main thread communication. +""" + +import json +import sys + +import js +from polyscript import config as _polyscript_config +from polyscript import js_modules +from pyscript.util import NotSupported + +RUNNING_IN_WORKER = not hasattr(js, "document") +"""Detect execution context: True if running in a worker, False if main thread.""" + +config = json.loads(js.JSON.stringify(_polyscript_config)) +"""Parsed and normalized configuration.""" +if isinstance(config, str): + config = {} + +js_import = None +"""Function to import JavaScript modules dynamically.""" + +window = None +"""The `window` object (proxied if in a worker).""" + +document = None +"""The `document` object (proxied if in a worker).""" + +sync = None +"""Sync utilities for worker-main thread communication (only in workers).""" + +# Detect and add Python interpreter type to config. +if "MicroPython" in sys.version: + config["type"] = "mpy" +else: + config["type"] = "py" + + +class _JSModuleProxy: + """ + Proxy for JavaScript modules imported via js_modules. + + This allows Python code to import JavaScript modules using Python's + import syntax: + + ```python + from pyscript.js_modules lodash import debounce + ``` + + The proxy lazily retrieves the actual JavaScript module when accessed. + """ + + def __init__(self, name): + """ + Create a proxy for the named JavaScript module. + """ + self.name = name + + def __getattr__(self, field): + """ + Retrieve a JavaScript object/function from the proxied JavaScript + module via the given `field` name. + """ + # Avoid Pyodide looking for non-existent special methods. + if not field.startswith("_"): + return getattr(getattr(js_modules, self.name), field) + return None + + +# Register all available JavaScript modules in Python's module system. +# This enables: from pyscript.js_modules.xxx import yyy +for module_name in js.Reflect.ownKeys(js_modules): + sys.modules[f"pyscript.js_modules.{module_name}"] = _JSModuleProxy(module_name) +sys.modules["pyscript.js_modules"] = js_modules + + +# Context-specific setup: Worker vs Main Thread. +if RUNNING_IN_WORKER: + import polyscript + + # PyWorker cannot be created from within a worker. + PyWorker = NotSupported( + "pyscript.PyWorker", + "pyscript.PyWorker works only when running in the main thread", + ) + + # Attempt to access main thread's window and document via SharedArrayBuffer. + try: + window = polyscript.xworker.window + document = window.document + js.document = document + + # Create js_import function that runs imports on the main thread. + js_import = window.Function( + "return (...urls) => Promise.all(urls.map((url) => import(url)))" + )() + + except: + # SharedArrayBuffer not available - window/document cannot be proxied. + sab_error_message = ( + "Unable to use `window` or `document` in worker. " + "This requires SharedArrayBuffer support. " + "See: https://docs.pyscript.net/latest/faq/#sharedarraybuffer" + ) + js.console.warn(sab_error_message) + window = NotSupported("pyscript.window", sab_error_message) + document = NotSupported("pyscript.document", sab_error_message) + + # Worker-specific utilities for main thread communication. + sync = polyscript.xworker.sync + + def current_target(): + """ + Get the current output target in worker context. + """ + return polyscript.target + +else: + # Main thread context setup. + import _pyscript + from _pyscript import PyWorker as _PyWorker + from pyscript.ffi import to_js + + js_import = _pyscript.js_import + + def PyWorker(url, **options): + """ + Create a Web Worker running Python code. + + This spawns a new worker thread that can execute Python code + found at the `url`, independently of the main thread. The + `**options` can be used to configure the worker. + + ```python + from pyscript import PyWorker + + + # Create a worker to run background tasks. + # (`type` MUST be either `micropython` or `pyodide`) + worker = PyWorker("./worker.py", type="micropython") + ``` + + PyWorker **can only be created from the main thread**, not from + within another worker. + """ + return _PyWorker(url, to_js(options)) + + # Main thread has direct access to window and document. + window = js + document = js.document + + # sync is not available in main thread (only in workers). + sync = NotSupported( + "pyscript.sync", "pyscript.sync works only when running in a worker" + ) + + def current_target(): + """ + Get the current output target in main thread context. + """ + return _pyscript.target diff --git a/pyscript/display.py b/pyscript/display.py new file mode 100644 index 0000000..69efd5d --- /dev/null +++ b/pyscript/display.py @@ -0,0 +1,261 @@ +""" +Display Pythonic content in the browser. + +This module provides the `display()` function for rendering Python objects +in the web page. The function introspects objects to determine the appropriate +[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types) +and rendering method. + +Supported MIME types: + +- `text/plain`: Plain text (HTML-escaped) +- `text/html`: HTML content +- `image/png`: PNG images as data URLs +- `image/jpeg`: JPEG images as data URLs +- `image/svg+xml`: SVG graphics +- `application/json`: JSON data +- `application/javascript`: JavaScript code (discouraged) + +The `display()` function uses standard Python representation methods +(`_repr_html_`, `_repr_png_`, etc.) to determine how to render objects. +Objects can provide a `_repr_mimebundle_` method to specify preferred formats +like this: + +```python +def _repr_mimebundle_(self): + return { + "text/html": "Bold HTML", + "image/png": "", + } +``` + +Heavily inspired by +[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html). +""" + +import base64 +import html +import io +from collections import OrderedDict +from pyscript.context import current_target, document, window +from pyscript.ffi import is_none + + +def _render_image(mime, value, meta): + """ + Render image (`mime`) data (`value`) as an HTML img element with data URL. + Any `meta` attributes are added to the img tag. + + Accepts both raw bytes and base64-encoded strings for flexibility. + """ + if isinstance(value, bytes): + value = base64.b64encode(value).decode("utf-8") + attrs = "".join([f' {k}="{v}"' for k, v in meta.items()]) + return f'' + + +# Maps MIME types to rendering functions. +_MIME_TO_RENDERERS = { + "text/plain": lambda v, m: html.escape(v), + "text/html": lambda v, m: v, + "image/png": lambda v, m: _render_image("image/png", v, m), + "image/jpeg": lambda v, m: _render_image("image/jpeg", v, m), + "image/svg+xml": lambda v, m: v, + "application/json": lambda v, m: v, + "application/javascript": lambda v, m: f" + + + +``` + +Dynamically creating named workers: + +```python +from pyscript import create_named_worker + + +# Create a worker from a Python file. +worker = await create_named_worker( + src="./background_tasks.py", + name="task-processor" +) + +# Use the worker's exported functions. +result = await worker.process_data([1, 2, 3, 4, 5]) +print(result) +``` + +Key features: +- Access (`await`) named workers via dictionary-like syntax. +- Dynamically create workers from Python. +- Cross-interpreter support (Pyodide and MicroPython). + +Worker access is asynchronous - you must `await workers[name]` to get +a reference to the worker. This is because workers may not be ready +immediately at startup. +""" + +import js +import json +from polyscript import workers as _polyscript_workers + + +class _ReadOnlyWorkersProxy: + """ + A read-only proxy for accessing named web workers. Use + `create_named_worker()` to create new workers found in this proxy. + + This provides dictionary-like access to named workers defined in + the page. It handles differences between Pyodide and MicroPython + implementations transparently. + + (See: https://github.com/pyscript/pyscript/issues/2106 for context.) + + The proxy is read-only to prevent accidental modification of the + underlying workers registry. Both item access and attribute access are + supported for convenience (especially since HTML attribute names may + not be valid Python identifiers). + + ```python + from pyscript import workers + + # Access a named worker. + my_worker = await workers["worker-name"] + result = await my_worker.some_function() + + # Alternatively, if the name works, access via attribute notation. + my_worker = await workers.worker_name + result = await my_worker.some_function() + ``` + + **This is a proxy object, not a dict**. You cannot iterate over it or + get a list of worker names. This is intentional because worker + startup timing is non-deterministic. + """ + + def __getitem__(self, name): + """ + Get a named worker by `name`. It returns a promise that resolves to + the worker reference when ready. + + This is useful if the underlying worker name is not a valid Python + identifier. + + ```python + worker = await workers["my-worker"] + ``` + """ + return js.Reflect.get(_polyscript_workers, name) + + def __getattr__(self, name): + """ + Get a named worker as an attribute. It returns a promise that resolves + to the worker reference when ready. + + This allows accessing workers via dot notation as an alternative + to bracket notation. + + ```python + worker = await workers.my_worker + ``` + """ + return js.Reflect.get(_polyscript_workers, name) + + +# Global workers proxy for accessing named workers. +workers = _ReadOnlyWorkersProxy() +"""Global proxy for accessing named web workers.""" + + +async def create_named_worker(src, name, config=None, type="py"): + """ + Dynamically create a web worker with a `src` Python file, a unique + `name` and optional `config` (dict or JSON string) and `type` (`py` + for Pyodide or `mpy` for MicroPython, the default is `py`). + + This function creates a new web worker by injecting a `