diff --git a/.github/workflows/update_docs.yml b/.github/workflows/update_docs.yml index b6a3c98..bf8a7ec 100644 --- a/.github/workflows/update_docs.yml +++ b/.github/workflows/update_docs.yml @@ -4,13 +4,16 @@ on: push: branches: - main + paths-ignore: + - '.github/**' + workflow_dispatch: jobs: update-docs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: conda-incubator/setup-miniconda@v2 @@ -32,7 +35,7 @@ jobs: - name: Update "gh-pages" branch shell: bash -l {0} run: | - git config user.name tedpatrick - git config user.email tpatrick@anaconda.com + git config user.name ${{ secrets.DOCS_GITHUB_USER }} + git config user.email ${{ secrets.DOCS_GITHUB_EMAIL }} git fetch origin gh-pages --depth=1 mike deploy --push --update-aliases ${{ steps.version.outputs.version }} latest diff --git a/.gitignore b/.gitignore index 9b6079a..bf99dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ _env site -.DS_Store \ No newline at end of file +.DS_Store +bin +lib +lib64 +pyvenv.cfg diff --git a/Makefile b/Makefile deleted file mode 100644 index af47bc0..0000000 --- a/Makefile +++ /dev/null @@ -1,46 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build -CONDA_ENV ?= _env - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -env := $(CONDA_ENV) -conda_run := conda run -p $(env) - -setup: - @if [ -z "$${CONDA_SHLVL:+x}" ]; then echo "Conda is not installed." && exit 1; fi - $(CONDA_EXE) env $(shell [ -d $(env) ] && echo update || echo create) -p $(env) --file environment.yml - -clean: - rm -rf $(BUILDDIR) - -clean-all: clean - rm -rf $(env) *.egg-info - -shell: - @export CONDA_ENV_PROMPT='<{name}>' - @echo 'conda activate $(env)' - -htmlserve: html - @echo 'visit docs at http://localhost:8080' - python -m http.server -d "$(BUILDDIR)/html/" 8080 - -livehtml: - sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - - -.PHONY: help Makefile setup clean clean-all shell - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md index 07e80b9..c0f8517 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,63 @@ # PyScript documentation -Welcome to the PyScript documentation directory, where you can find -and contribute to discussions around PyScript and related topics. +Welcome to the PyScript documentation repository. + +This source code becomes the official PyScript documentation hosted here: + +[https://docs.pyscript.net](https://docs.pyscript.net/) + +Contribute prose and participate in discussions about the written support of +PyScript and related topics. ## Getting started Before you start contributing to the documentation, it's worthwhile to -take a look at the general contributing guidelines for the PyScript project. You can find these guidelines here +take a look at the general contributing guidelines for the PyScript project. +You can find these guidelines here [Contributing Guidelines](https://github.com/pyscript/pyscript/blob/main/CONTRIBUTING.md) ## Setup The `docs` directory in the pyscript repository contains a -[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) documentation project. Material is a system -that takes plaintext files containing documentation written in Markdown, along with -static files like templates and themes, to build the static end result. - -To setup the documentation development environment simply run `make setup` from this folder and, once it's done, -activate your environment by running `conda activate ./_env` +[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) +documentation project. Material is a system that takes plaintext files +containing documentation written in Markdown, along with static files like +templates and themes, to build the static end result. + +To setup the documentation development environment simply create a new +virtual environment, then `pip install -r requirements.txt` (from in the root +of this repository). + +```sh +# example of a simple virtual environment +# creation from the root of this project +python -m venv . +./bin/pip install --upgrade setuptools +./bin/pip install -r requirements.txt +``` ## Build -Simply run `mkdocs serve` +Simply run `mkdocs serve` or `./bin/mkdocs serve`. ## Cross-referencing -You can link to other pages in the documentation by using the `{doc}` role. For example, to link to the `docs/README.md` file, you would use: +Link to other pages in the documentation by using the `{doc}` role. For +example, to link to the `docs/README.md` file, you would use: ```markdown {doc}`docs/README.md` ``` -You can also cross-reference the python glossary by using the `{term}` role. For example, to link to the `iterable` term, you would use: +Cross-reference the Python glossary by using the `{term}` role. For example, to +link to the `iterable` term, you would use: ```markdown {term}`iterable` ``` -You can also cross-reference functions, methods or data attributes by using the `{attr}` for example: +Cross-reference functions, methods or data attributes by using the `{attr}` for +example: ```markdown {py:func}`repr` diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..8a992d3 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,1232 @@ +# 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/assets/images/micropython.png b/docs/assets/images/micropython.png new file mode 100644 index 0000000..2394715 Binary files /dev/null and b/docs/assets/images/micropython.png differ diff --git a/docs/assets/images/py-terminal.gif b/docs/assets/images/py-terminal.gif new file mode 100644 index 0000000..8eaf0b5 Binary files /dev/null and b/docs/assets/images/py-terminal.gif differ diff --git a/docs/assets/images/pyeditor-stop.gif b/docs/assets/images/pyeditor-stop.gif new file mode 100644 index 0000000..8c0f0b7 Binary files /dev/null and b/docs/assets/images/pyeditor-stop.gif differ diff --git a/docs/assets/images/pyeditor1.gif b/docs/assets/images/pyeditor1.gif new file mode 100644 index 0000000..c9d35dd Binary files /dev/null and b/docs/assets/images/pyeditor1.gif differ diff --git a/docs/assets/images/pyodide.png b/docs/assets/images/pyodide.png new file mode 100644 index 0000000..587a2d8 Binary files /dev/null and b/docs/assets/images/pyodide.png differ diff --git a/docs/assets/images/pyscript.com.png b/docs/assets/images/pyscript.com.png new file mode 100644 index 0000000..37c04ea Binary files /dev/null and b/docs/assets/images/pyscript.com.png differ diff --git a/docs/assets/images/pyterm1.png b/docs/assets/images/pyterm1.png new file mode 100644 index 0000000..440797c Binary files /dev/null and b/docs/assets/images/pyterm1.png differ diff --git a/docs/assets/images/pyterm2.gif b/docs/assets/images/pyterm2.gif new file mode 100644 index 0000000..86ce45d Binary files /dev/null and b/docs/assets/images/pyterm2.gif differ diff --git a/docs/assets/images/pyterm3.gif b/docs/assets/images/pyterm3.gif new file mode 100644 index 0000000..40c9939 Binary files /dev/null and b/docs/assets/images/pyterm3.gif differ diff --git a/docs/beginning-pyscript.md b/docs/beginning-pyscript.md index 06af582..779e597 100644 --- a/docs/beginning-pyscript.md +++ b/docs/beginning-pyscript.md @@ -1,28 +1,28 @@ # Beginning PyScript -PyScript is a platform for Python in browsers. All you and your users need is -a modern web browser. +PyScript is a platform for running +Python +in modern web browsers. -To distribute a PyScript application, you simply host it like a static web page -and users click on the link to your application. PyScript and the browser do -the rest. +Create apps with a +PyScript development environment: +write code, curate the project's assets, and test your application. -To start creating apps with PyScript you use a development environment -where you write your code, configure the project's assets, and distribute your -application. +To distribute a PyScript application, host it on the web, then click +on the link to your application. PyScript and the browser do the rest. This page covers these core aspects of PyScript in a beginner friendly manner. We only assume you know how to use a browser and edit text. !!! note - The easiest way to get the a full PyScript development environment and - hosting provider, is to use [pyscript.com](pyscript.com) in your browser. + The easiest way to get a PyScript development environment and hosting, is + to use [pyscript.com](https://pyscript.com) in your browser. It is a free service that helps you create new projects from templates, and then edit, preview and deploy your apps with a unique link. - While the core features of [pyscript.com](pyscript.com) will always be + While the core features of [pyscript.com](https://pyscript.com) will always be free, additional paid-for capabilities directly support and sustain the PyScript open source project. Commercial and educational support is also available. @@ -31,32 +31,26 @@ We only assume you know how to use a browser and edit text. All PyScript applications need three things: -1. A `pyscript.toml` file that configures your application. -2. An `index.html` file that is served to your browser. +1. An `index.html` file that is served to your browser. +2. A description of the Python environment in which your application will run. + This is usually specified by a `pyscript.json` or `pyscript.toml` file. 3. Python code (usually in a file called something like `main.py`) that defines how your application works. -You could create these files with your favourite code editor on your local file -system. Alternatively, using [pyscript.com](pyscript.com) will take away all -the pain of organising, previewing and deploying your application. +Create these files with your favourite code editor on your local file system. +Alternatively, [pyscript.com](https://pyscript.com) will take away all the pain +of organising, previewing and deploying your application. -If you decide to use [pyscript.com](pyscript.com) (recommended for first -steps), once signed in, create a new project by pressing the "+" button on the -left hand side below the site's logo. You'll be presented with a page -containing three columns (listing your files, showing your code and previewing -the app). The "save" and "run" buttons do exactly what you'd expect. - -If you're using your local file system, you'll need a way to view how your -application looks in your browser. To make this work you'll need to serve these -files, and if you already have Python installed it can be accomplished with -the following command run from your terminal and in the same directory as your -files: +If you're using your local file system, you'll need a way to view your +application in your browser. If you already have Python installed on +your local machine, serve your files with the following command run from your +terminal and in the same directory as your files: ```sh python3 -m http.server ``` -Then point your browser at [http://localhost:8000](localhost:8000). Remember to +Point your browser at [http://localhost:8000](localhost:8000). Remember to refresh the page (`CTRL-R`) to see any updates you may have made. !!! note @@ -65,21 +59,47 @@ refresh the page (`CTRL-R`) to see any updates you may have made. [Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) can be used to reload the page as you edit your files. -Let's build a simple PyScript application that translates English into Pirate -speak. In order to do this we'll make use of the -[arrr](https://arrr.readthedocs.io/en/latest/) library. + Alternatively, if you have an account on [GitHub](https://github.com) you + could use VSCode in your browser as a + [PyScript aware "CodeSpace"](https://github.com/ntoll/codespaces-project-template-pyscript/) + (just follow the instructions in the README file). + +If you decide to use [pyscript.com](https://pyscript.com) (recommended for +first steps), once signed in, create a new project by pressing the "+" button +on the left hand side below the site's logo. You'll be presented with a page +containing three columns (listing your files, showing your code and previewing +the app). The "save" and "run" buttons do exactly what you'd expect. + +![PyScript.com](assets/images/pyscript.com.png) + +Let's build a simple PyScript application that translates English 🇬🇧 into +Pirate 🏴‍☠️ speak. In order to do this we'll make use of the +[arrr](https://arrr.readthedocs.io/en/latest/) library. By building this app +you'll be introduced to all the core concepts of PyScript at an introductory +level. -### pyscript.toml +You can see this application embedded into the page below (try it out!): -This file tells PyScript and your browser about various configurable aspects of -your application. In this specific example, the only thing we need to configure -is that we're using the `arrr` module. + + +Let's explore each of the three files that make this app work. + +### pyscript.json + +This file tells PyScript and your browser about various +[configurable aspects](../user-guide/configuration) +of your application. Put simply, it tells PyScript what it needs in order to run +your application. The only thing we need to show is that we require the third +party `arrr` module to do the +[actual translation](https://arrr.readthedocs.io/en/latest/). We do this by putting `arrr` as the single entry in a list of required -`packages`, so the content of `pyscript.toml` looks like this: +`packages`, so the content of `pyscript.json` looks like this: -``` toml title="pyscript.toml" -packages = ["arrr" ] +``` json title="pyscript.json" +{ + "packages": ["arrr"] +} ``` ### index.html @@ -96,8 +116,9 @@ module in the document's `` tag: - Codestin Search App - + Codestin Search App + + @@ -107,18 +128,18 @@ module in the document's `` tag: ``` -Notice that the `` of the document is empty. It's in here that we put -standard HTML content to define our user interface, so the `` now looks -like: +Notice that the `` of the document is empty except for the TODO comment. +It's in here that we put standard HTML content to define our user interface, so +the `` now looks like: ``` html -

Arrr

+

Polyglot 🦜 💬 🇬🇧 ➡️ 🏴‍☠️

Translate English into Pirate speak...

- + ``` @@ -130,12 +151,13 @@ application's output. There's something strange about the `
- + ``` @@ -166,11 +188,11 @@ behaviour we need to write some Python. Specifically, we need to define the ### main.py -The behaviour of the appication is defined in `main.py`. It looks like this: +The behaviour of the application is defined in `main.py`. It looks like this: ``` python linenums="1" title="main.py" import arrr -from js import document +from pyscript import document def translate_english(event): @@ -183,22 +205,28 @@ def translate_english(event): It's not very complicated Python code. On line 1 the `arrr` module is imported so we can do the actual English to -Pirate translation. Line 2 imports the `document` object from the browser's -global JavaScript context. Put simply, the `document` allows us to reach into -the things on the web page defined in `index.html`. Finally, on line 5 the -`translate_english` function is defined. +Pirate translation. If we hadn't told PyScript to download the `arrr` module +in our `pyscript.json` configuration file, this line would cause an error. +PyScript has ensured our environment is set up with the expected `arrr` module. + +Line 2 imports the `document` object. The `document` allows us to reach into +the things on the web page defined in `index.html`. + +Finally, on line 5 the `translate_english` function is defined. The `translate_english` function takes a single parameter called -`event` that represents the user's click of the button (but which we don't +`event`. This represents the user's click of the button (but which we don't actually use). Inside the body of the function we first get a reference to the `input` -element with the `document.querySelector` function that takes `#english` as its +element with the [`document.querySelector` function](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) +that takes `#english` as its parameter (indicating we want the element with the id "english"). We assign the result to `input_text`, then extract the user's `english` from the `input_text`'s `value`. Next, we get a reference called `output_div` that points to the `div` element with the id "output". Finally, we assign the -`innerText` of the `output_div` to the result of calling `arrr.translate` +`innerText` of the `output_div` to the result of calling +[`arrr.translate`](https://arrr.readthedocs.io/en/latest/#arrr.translate) (to actually translate the `english` to something piratical). That's it! @@ -207,33 +235,42 @@ That's it! ### PyScript.com -If you're using [pyscript.com](pyscript.com), you should save all your files +If you're using [pyscript.com](https://pyscript.com), you should save all your files and click the "run" button. Assuming you've copied the code properly, you -should have a fine old time translating English to Pirate-ish. +should have a fine old time using "Polyglot 🦜" to translate English to +Pirate-ish. -Alternatively, [click here to see a working example of this app](https://ntoll.pyscriptapps.com/piratical/latest/). +Alternatively, [click here to see a working example of this app](https://ntoll.pyscriptapps.com/piratical/v5/). Notice that the bottom right hand corner contains a link to view the code on -[pyscript.com](pyscript.com). +[pyscript.com](https://pyscript.com). Why not explore the code, copy it to your own +account and change it to your satisfaction? ### From a web server -Just host the three files (`pyscript.toml`, `index.html` +Just host the three files (`pyscript.json`, `index.html` and `main.py`) in the same directory on a static web server somewhere. -Clearly, we recommend you use [pyscript.com](pyscript.com) for this, but any +Clearly, we recommend you use [pyscript.com](https://pyscript.com) for this, but any static web host will do (for example, [GitHub Pages](https://pages.github.com/), [Amazon's S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html), [Google Cloud](https://cloud.google.com/storage/docs/hosting-static-website) or [Microsoft's Azure](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website)). +## Run PyScript Offline + +To run PyScript offline, without the need of a CDN or internet connection, read +the [Run PyScript Offline](user-guide/offline.md) section of the user +guide. + ## Conclusion Congratulations! -You have just created your first PyScript app, and understand -the core concepts needed to build yet more interesting things of your own. +You have just created your first PyScript app. We've explored the core concepts +needed to build yet more interesting things of your own. PyScript is extremely powerful, and these beginner steps only just scratch the surface. To learn about PyScript in more depth, check out -[our user guide](/user-guide). +[our user guide](user-guide/index.md) or +[explore our example applications](../examples). diff --git a/docs/contributing.md b/docs/contributing.md index f5e5dd0..2a3190c 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -4,7 +4,7 @@ Thank you for wanting to contribute to the PyScript project! ## Code of conduct -The [PyScript Code of Conduct](/conduct) governs the project and everyone +The [PyScript Code of Conduct](conduct.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document. @@ -18,7 +18,7 @@ Bugs are tracked on the !!! warning "Check first" - Please check your bug has not already been reported by someone else byi + Please check your bug has not already been reported by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly. @@ -50,6 +50,9 @@ If you have an interesting project, a cool hack, or an innovative way to implement a new capability or feature, please share your work and tell us about it so we can celebrate and amplify your contribution. +- Show off your creation at our fortnightly + [PyScript FUN](https://discord.com/events/972017612454232116/1227275336115556402) + community call on discord. - Write up your creation in a blog post. - Share it on social media. - Create a demo video. @@ -63,11 +66,16 @@ Please reach out to us if you'd like advice and feedback. As an open source project, PyScript has community at its core. In addition to the ways to engage already outlined, you could join our live community calls. -- PyScript Community Call - weekly on Tuesdays. A coordination call for anyone - participating in the PyScript project. -- PyScript FUN - held every two weeks on a Thursday, this call is an informal, +- [PyScript Community Call](https://discord.com/events/972017612454232116/1227274094366556279) + - weekly on Tuesdays. A coordination call for anyone participating in the + PyScript project. +- [Community Engagement Call](https://discord.com/events/972017612454232116/1227275045965922376) + - weekly on Wednesdays. A non-technical meeting where we coordinate how the + PyScript project engages with, nourishes and grows our wonderful community. +- [PyScript FUN](https://discord.com/events/972017612454232116/1227275336115556402) + - held every two weeks on a Thursday, this call is an informal, supportive and often humorous "show and tell" of community created projects, - hacks and plugins. + hacks, feedback and plugins. Announcement of connection details is made via the PyScript [discord server](https://discord.gg/HxvBtukrg2). @@ -117,15 +125,17 @@ git remote add upstream https://github.com/pyscript/pyscript.git git pull upstream main ``` -* ??? +* Contribute changes using the + [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow) + model of coding collaboration. ### License terms for contributions PyScrcipt welcomes contributions, suggestions, and feedback. All contributions, suggestions, and feedback you submitted are accepted under the -[Apache 2.0](/license) license. You represent that if you do not own +[Apache 2.0](license.md) license. You represent that if you do not own copyright in the code that you have the authority to submit it under the -[Apache 2.0](/license) license. All feedback, suggestions, or contributions +[Apache 2.0](license.md) license. All feedback, suggestions, or contributions are not confidential. ### Becoming a maintainer @@ -133,16 +143,10 @@ are not confidential. Contributors are invited to be maintainers of the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the -[code of conduct](./conduft.md). New maintainers are invited by a 3/4 vote of +[code of conduct](conduct.md). New maintainers are invited by a 3/4 vote of the existing maintainers. ## Trademarks The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). - ---- - -Part of MVG-0.1-beta. -Made with love by GitHub. Licensed under the -[CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/docs/developers.md b/docs/developers.md new file mode 100644 index 0000000..f6f031a --- /dev/null +++ b/docs/developers.md @@ -0,0 +1,313 @@ +# Developer Guide + +This page explains the technical and practical requirements and processes +needed to contribute to PyScript. + +!!! info + + In the following instructions, we assume familiarity with `git`, + [GitHub](https://github.com/pyscript/pyscript), the command line and other + common development concepts, tools and practices. + + For those who come from a non-Pythonic technical background (for example, + you're a JavaScript developer), we will explain Python-isms as we go along + so you're contributing with confidence. + + If you're unsure, or encounter problems, please ask for help on our + [discord server](https://discord.gg/HxvBtukrg2). + +## Welcome + +We are a diverse, inclusive coding community and welcome contributions from +_anyone_ irrespective of their background. If you're thinking, "but they don't +mean me", _then we especially mean YOU_. Our diversity means _you will meet +folks in our community who are different to yourself_. Therefore, thoughtful +contributions made in good faith, and engagement with respect, care and +compassion wins every time. + +* If you're from a background which isn't well-represented in most geeky + groups, get involved - _we want to help you make a difference_. +* If you're from a background which **is** well-represented in most geeky + groups, get involved - _we want your help making a difference_. +* If you're worried about not being technical enough, get involved - _your + fresh perspective will be invaluable_. +* If you need help with anything, get involved - _we welcome questions asked + in good faith, and will move mountains to help_. +* If you're unsure where to start, get involved - _we have [many ways to + contribute](contributing.md)_. + +All contributors are expected to follow our [code of conduct](conduct.md). + +## Setup +The following steps create a working development environment for PyScript. It +is through this environment that you contribute to PyScript. You can choose +between two options for setting up your environment. + +!!! danger + + The following commands work on Unix like operating systems (like MacOS or + Linux). **If you are a Microsoft Windows user please use the + [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/about) + with the following instructions.** + +### Create a virtual environment + +* A Python [virtual environment](https://docs.python.org/3/library/venv.html) + is a computing "sandbox" that safely isolates your work. PyScript's + development makes use of various Python based tools, so both + [Python](https://python.org) and a virtual environment is needed. There are + many tools to help manage these environments, but the standard way to create + a virtual environment is to use this command in your terminal: + + ```sh + python3 -m venv my_pyscript_dev_venv + ``` + + !!! warning + + Replace `my_pyscript_dev_venv` with a meaningful name for the + virtual environment, that works for you. + +* A `my_pyscript_dev_venv` directory containing the virtual environment's + "stuff" is created as a subdirectory of your current directory. Next, + activate the virtual environment to ensure your development activities happen + within the context of the sandbox: + + ```sh + source my_pyscript_dev_venv/bin/activate + ``` + +* The prompt in your terminal will change to include the name of your virtual + environment indicating the sandbox is active. To deactivate the virtual + environment just type the following into your terminal: + + ```sh + deactivate + ``` + +### Option 2: Create a conda environment +**This option will install Python and NodeJS for you, so you don't need to have them +pre-installed on your system.** + +* If you prefer using [conda](https://docs.conda.io/en/latest/) for environment management, +you can create a conda environment that includes both Python and NodeJS: +* + ```sh + conda create --name pyscript python nodejs + conda activate pyscript + ``` + + !!! warning + + Replace `pyscript` with a meaningful name for the conda environment, that works for you. + +* This creates a new environment with both Python and NodeJS installed. The prompt in your +terminal will change to include the name of your conda environment indicating the sandbox is active. +* To deactivate the conda environment just type the following into your terminal: + + ```sh + conda deactivate + ``` + +* If you don't have conda installed, you can download and install +[Miniconda, Miniforge](https://docs.conda.io/projects/conda), or +[Anaconda](https://www.anaconda.com/download). + +!!! info + + The rest of the instructions on this page assume you are working in **an + activated virtual environment** for developing PyScript. + +### Prepare your repository + +* Create a [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) + of the + [PyScript github repository](https://github.com/pyscript/pyscript/fork) to + your own GitHub account. +* [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) + your newly forked version of the PyScript repository onto your + local development machine. For example, use this command in your terminal: + + ```sh + git clone https://github.com//pyscript + ``` + + !!! warning + + In the URL for the forked PyScript repository, remember to replace + `` with your actual GitHub username. + + !!! tip + + To help explain steps, we will use `git` commands to be typed into your + terminal / command line. + + The equivalent of these commands could be achieved through other means + (such as [GitHub's desktop client](https://desktop.github.com/)). How + these alternatives work is beyond the scope of this document. + +* Change into the root directory of your newly cloned `pyscript` repository: + + ```sh + cd pyscript + ``` + +* Add the original PyScript repository as your `upstream` to allow you to keep + your own fork up-to-date with the latest changes: + + ```sh + git remote add upstream https://github.com/pyscript/pyscript.git + ``` + +* If the above fails, try this alternative: + + ```sh + git remote remove upstream + git remote add upstream git@github.com:pyscript/pyscript.git + ``` + +* Pull in the latest changes from the main `upstream` PyScript repository: + + ```sh + git pull upstream main + ``` + +* Pyscript uses a `Makefile` to automate the most common development tasks. In + your terminal, type `make` to see what it can do. You should see something + like this: + + ```sh + There is no default Makefile target right now. Try: + + make setup - check your environment and install the dependencies. + make clean - clean up auto-generated assets. + make build - build PyScript. + make precommit-check - run the precommit checks (run eslint). + make test-integration - run all integration tests sequentially. + make fmt - format the code. + make fmt-check - check the code formatting. + ``` + +### Install dependencies + +* To install the required software dependencies for working on PyScript, in + your terminal type: + + ```sh + make setup + ``` + +* Updates from `npm` and then `pip` will scroll past telling you about + their progress installing the required packages. + + !!! warning + + The `setup` process checks the versions of Python, node + and npm. If you encounter a failure at this point, it's probably + because one of these pre-requisits is out of date on your system. + Please update! + +## Check code + +* To ensure consistency of code layout we use tools to both reformat and check + the code. + +* To ensure your code is formatted correctly: + + ```sh + make fmt + ``` + +* To check your code is formatted correctly: + + ```sh + make fmt-check + ``` + +* Finally, as part of the automated workflow for contributing + [pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) + pre-commit checks the source code. If this fails revise your PR. To run + pre-commit checks locally (before creating the PR): + + ```sh + make precommit-check + ``` + + This may also revise your code formatting. Re-run `make precommit-check` to + ensure this is the case. + +## Build PyScript + +* To turn the JavaScript source code found in the `pyscript.core` directory + into a bundled up module ready for the browser, type: + + ```sh + make build + ``` + + The resulting assets will be in the `pyscript.core/dist` directory. + +## Run the tests + +* The integration tests for PyScript are started with: + + ```sh + make test + ``` + + (This essentially runs the `npm run test:integration` command in the right + place. This is defined in PyScript's `package.json` file.) + + Tests are found in the `core/tests` directory. These are organised into + three locations: + + 1. `python` - the Python based test suite to exercise Python code + **within** PyScript. + 2. `javascript` - JavaScript tests to exercise PyScript itself, in the + browser. + 3. `manual` - containing tests to run manually in a browser, due to the + complex nature of the tests. + + We use [Playwright](https://playwright.dev/) to automate the running of the + Python and JavaScript test suites. We use + [uPyTest](https://github.com/ntoll/upytest) as a test framework for the + Python test suite. uPyTest is a "PyTest inspired" framework for running + tests in the browser on both MicroPython and Pyodide. + + The automated (Playwright) tests are specified in the + `tests/integration.spec.js` file. + +## Documentation + +* Documentation for PyScript (i.e. what you're reading right now), is found + in a separate repository: + [https://github.com/pyscript/docs](https://github.com/pyscript/docs) + +* The documentation's `README` file contains instructions for setting up a + development environment and contributing. + +## Contributing + +* We have [suggestions for how to contribute to PyScript](contributing.md). Take + a read and dive in. + +* Please make sure you discuss potential contributions *before* you put in + work. We don#t want folks to waste their time or re-invent the wheel. + +* Technical discussions happen [on our discord server](https://discord.gg/HxvBtukrg2) + and in the + [discussions section](https://github.com/pyscript/pyscript/discussions) of + our GitHub repository. + +* Every Tuesday is a [technical community video call](https://discord.com/events/972017612454232116/1227274094366556279), + the details of which are posted onto + the discord server. Face to face technical discussions happen here. + +* Every Wednesday is a [non-technical community engagement call](https://discord.com/events/972017612454232116/1227275045965922376), + in which we organise how to engage with, grow and nourish our community. + +* Every two weeks, on a Thursday, is a + [PyScript FUN call](https://discord.com/events/972017612454232116/1227275336115556402), + the details of which + are also posted to discord. Project show-and-tells, cool hacks, new features + and a generally humorous and creative time is had by all. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..049ebc9 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,62 @@ +# Example applications + +A curated list of example applications that demonstrate various features of +PyScript can be found [on PyScript.com](https://pyscript.com/@examples). + +The examples are (links take you to the code): + +### Simple: + +* [Hello world](https://pyscript.com/@examples/hello-world/latest) + * uses included datetime module. No additional packages. +* [WebGL Icosahedron](https://pyscript.com/@examples/webgl-icosahedron/latest) + * uses [three.js](https://threejs.org/) imported as a module in toml) +* [Pandas dataframe fun](https://pyscript.com/@examples/pandas/latest) + * uses [pandas](https://pandas.pydata.org/) from pypi +* [Matplotlib example](https://pyscript.com/@examples/matplotlib/latest) + * uses [matplotlib](https://matplotlib.org/) +* [Todo](https://pyscript.com/@examples/todo-app/latest) + * uses pyweb. No additional packages. +* [Tic Tac Toe](https://pyscript.com/@examples/tic-tac-toe/latest) + * uses pyweb. No additional packages. +* [Pyscript Jokes](https://pyscript.com/@examples/pyscript-jokes/latest) + * uses pyweb and [pyjokes](https://pyjok.es/) +* [D3 visualization](https://pyscript.com/@examples/d3-visualization/latest) + * uses [d3](https://d3js.org/) + * mixes javascript code with python code. imports d3 from javascript. +* [Import antigravity](https://pyscript.com/@examples/antigravity/latest) + * uses svg simply +* [API proxy tutorial](https://pyscript.com/@examples/api-proxy-tutorial/latest) + * uses fetch +* [API proxy and secrets tutorial](https://pyscript.com/@examples/api-proxy-and-secrets-tutorial/latest) + * uses fetch + +### More complex: + +* [Numpy fractals](https://pyscript.com/@examples/fractals-with-numpy-and-canvas/latest) + * uses [numpy](https://numpy.org/), [sympy](https://www.sympy.org/en/index.html) from pypi +* [Simple slider panel](https://pyscript.com/@examples/simple-panel/latest) + * uses [Panel](https://panel.holoviz.org/) and [Bokeh](https://bokeh.org/) from pypi and loads in index.html +* [Streaming data panel](https://pyscript.com/@examples/streaming-in-panel/latest) + * uses [Panel](https://panel.holoviz.org/), [Bokeh](https://bokeh.org/) [numpy](https://numpy.org/), [pandas](https://pandas.pydata.org/) from pypi + * loads bokeh, panel, tabulator in index.html +* [KMeans in a panel](https://pyscript.com/@examples/kmeans-in-panel/latest) + * uses [Bokeh](https://bokeh.org/), [altair](https://altair-viz.github.io/), [numpy](https://numpy.org/), [pandas](https://pandas.pydata.org/), [scikit-learn](https://scikit-learn.org/stable/), [Panel](https://panel.holoviz.org/) from pypi + * loads panel, bootstrap, vega, tabulator, bokeh in index.html +* [New York Taxi panel (WebGL)](https://pyscript.com/@examples/nyc-taxi-panel-deckgl/latest) + * uses a mixture of pypi and direct load packages in index.html + * [Bokeh](https://bokeh.org/), [numpy](https://numpy.org/), [pandas](https://pandas.pydata.org/), [Panel](https://panel.holoviz.org/), [deck-gl](https://deck.gl/) + * deckGL, bokeh are loaded directly in index.html +* [Folium geographical data](https://pyscript.com/@examples/folium/latest) + * uses [folium](https://python-visualization.github.io/folium/latest/), [pandas](https://pandas.pydata.org/) from pypi +* [Bokeh data plotting](https://pyscript.com/@examples/bokeh/latest) + * uses [pandas](https://pandas.pydata.org/), [Bokeh](https://bokeh.org/), [xyzservices](https://github.com/geopandas/xyzservices) from pypi +* [Altair data plotting](https://pyscript.com/@examples/altair/latest) + * uses [altair](https://altair-viz.github.io/), [pandas](https://pandas.pydata.org/), [vega_datasets](https://github.com/altair-viz/vega_datasets) from pypi +* [Panel and hyplot](https://pyscript.com/@examples/panel-and-hvplot/latest) + * uses [Bokeh](https://bokeh.org/), [Panel](https://panel.holoviz.org/), [markdown-it-py](https://github.com/executablebooks/markdown-it-py), [numpy](https://numpy.org/), [pandas](https://pandas.pydata.org/), [hvplot](https://hvplot.holoviz.org/), [pyodide-http](https://pyodide.org/en/stable/usage/api/python-api/http.html) a fetch library. + * bokeh and panel are loaded in the index.html + +Notes: + - No micropython examples - all are pyodide + - No worker examples diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..bd8a941 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,1280 @@ +# FAQ + +This page contains the most common questions and "*gotchas*" asked on +[our Discord server](https://discord.gg/HxvBtukrg2), in +[our community calls](https://www.youtube.com/@PyScriptTV), or +within our community. + +There are two major areas we'd like to explore: +[common errors](#common-errors) and [helpful hints](#helpful-hints). + +## Common errors + +### Reading errors + +If your application doesn't run, and you don't see any error messages on the +page, you should check +[your browser's console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools). + +When reading an error message, the easy way to find out what's going on, +most of the time, is to read the last line of the error. + +```text title="A Pyodide error." +Traceback (most recent call last): + File "/lib/python311.zip/_pyodide/_base.py", line 501, in eval_code + .run(globals, locals) + ^^^^^^^^^^^^^^^^^^^^ + File "/lib/python311.zip/_pyodide/_base.py", line 339, in run + coroutine = eval(self.code, globals, locals) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 1, in +NameError: name 'failure' is not defined +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +```text title="A MicroPython error." +Traceback (most recent call last): + File "", line 1, in +NameError: name 'failure' isn't defined +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +In both examples, the code created a +[`NameError`](https://docs.python.org/3/library/exceptions.html#NameError) +because the object with the name `failure` did not exist. Everything above the +error message is potentially useful technical detail. + +With this context in mind, these are the most common errors users of PyScript +encounter. + +### SharedArrayBuffer + +This is the first and most common error users may encounter with PyScript: + +!!! failure + + Your application doesn't run and in + [your browser's console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools) + you see this message: + + ``` + Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer + ``` + +#### When + +This happens when you're unable to access objects in the main thread (`window` +and `document`) from code running in a web worker. + +This error happens because **the server delivering your PyScript application is +incorrectly configured** or **a `service-worker` attribute has not been used in +your `script` element**. + +Specifically, one of the following three problem situations applies to your +code: + +* Because of the way your web server is configured, the browser limits the use + of a technology called "Atomics" (you don't need to know how it works, just + that it may be limited by the browser). If there is a `worker` attribute in + your `script` element, and your Python code uses the `window` or `document` + objects (that actually exist on the main thread), then the browser limitation + on Atomics will cause the failure, unless you reconfigure your server. +* There is a ` +``` + +Alternatively, ensure any JavaScript code you reference uses `export ...` or +ask for an `.mjs` version of the code. All the various options and technical +considerations surrounding the use of JavaScript modules in PyScript are +[covered in our user guide](../user-guide/dom/#working-with-javascript). + +#### Why + +Even though the standard for JavaScript modules has existed since 2015, many +old and new libraries still produce files that are incompatible with such +modern and idiomatic standards. + +This isn't so much a technical problem, as a human problem as folks learn to +use the new standard and migrate old code away from previous and now +obsolete standards. + +While such legacy code exists, be aware that JavaScript code may require +special care. + +### Possible deadlock + +Users may encounter an error message similar to the following: + +!!! failure + + ``` + 💀🔒 - Possible deadlock if proxy.xyz(...args) is awaited + ``` + +#### When + +This error happens when your code on a worker and in the main thread are +[in a deadlock](https://en.wikipedia.org/wiki/Deadlock). Put simply, neither +fragment of code can proceed without waiting for the other. + +#### Why + +Let's assume a worker script contains the following Python code: + +```python title="worker: a deadlock example" +from pyscript import sync + +sync.worker_task = lambda: print('🔥 this is fine 🔥') + +# deadlock 💀🔒 +sync.main_task() +``` + +On the main thread, let's instead assume this code: + +```html title="main: a deadlock example" + +``` + +When the worker bootstraps and calls `sync.main_task()` on the main thread, it +blocks until the result of this call is returned. Hence it cannot respond to +anything at all. However, in the code on the main thread, the +`sync.worker_task()` in the worker is called, but the worker is blocked! Now +the code on both the main thread and worker are mutually blocked and waiting +on each other. We are in a classic +[deadlock](https://en.wikipedia.org/wiki/Deadlock) situation. + +The moral of the story? Don't create such circular deadlocks! + +How? + +The mutually blocking calls cause the deadlock, so simply don't block. + +For example, on the main thread, let's instead assume this code: + +```html title="main: avoiding deadlocks" + +``` + +By scheduling the call to the worker (rather than awaiting it), it's possible +for the main thread to call functions defined in the worker in a non-blocking +manner, thus allowing the worker to also work in an unblocked manner and react +to such calls. We have resolved the mutual deadlock. + +### TypeError: crypto.randomUUID is not a function + +If PyScript fails to start and you look in the browser console, you may +find the following error: + +!!! failure + + ``` + main.js:43 Uncaught TypeError: crypto.randomUUID is not a function + at main.js:43:26 + ``` + +#### When + +This happens because PyScript uses the `crypto.randomUUID` function, and the +web page isn't served correctly. + +#### Why + +This error is _created by the browser_ because `crypto.randomUUID` requires a +secure context or localhost to use the latest web standards that are part of +PyScript's core (such as `crypto.randomUUID`). + +Put simply, your code should be served from a domain secured +with TLS (i.e. the domain name starts with `https` - use a service like +[let's encrypt](https://letsencrypt.org/) to address this) or from `localhost` +if developing and viewing your site on your development machine. + +This is something PyScript can't fix. Rather, it's how the web works and you +should always ensure your code is served in a secure manner. + +## Helpful hints + +This section contains common hacks or hints to make using PyScript easier. + +!!! Note + + We have an absolutely lovely PyScript contributor called + [Jeff Glass](https://github.com/jeffersglass) who maintains an exceptional + blog full of [PyScript recipes](https://pyscript.recipes/) with even more + use cases, hints, tips and solutions. Jeff also has a + [wonderful YouTube channel](https://www.youtube.com/@CodingGlass) full of + very engaging PyScript related content. + + If you cannot find what you are looking for here, please check Jeff's blog + as it's likely he's probably covered something close to the situation in + which you find yourself. + + Of course, if ever you meet Jeff in person, please buy him a beer and + remember to say a big "thank you". 🍻 + +### PyScript `latest` + +PyScript follows the [CalVer](https://calver.org/) convention for version +numbering. + +Put simply, it means each version is numbered according to when, in the +calendar, it was released. For instance, version `2024.4.2` was the _second_ +release in the month of April in the year 2024 (**not** the release on the 2nd +of April but the second release **in** April). + +It used to be possible to reference PyScript via a version called `latest`, +which would guarantee you always got the latest release. + +However, at the end of 2023, we decided to **stop supporting `latest` as a +way to reference PyScript**. We did this for two broad reasons: + +1. In the autumn of 2023, we release a completely updated version of PyScript + with some breaking changes. Folks who wrote for the old version, yet still + referenced `latest`, found their applications broke. We want to avoid this + at all costs. +2. Our release cadence is more regular, with around two or three releases a + month. Having transitioned to the new version of PyScript, we aim to avoid + breaking changes. However, we are refining and adding features as we adapt + to our users' invaluable feedback. + +Therefore, +[pinning your app's version of PyScript to a specific release](https://github.com/pyscript/pyscript/releases) +(rather than `latest`) ensures you get exactly the version of PyScript you +used when writing your code. + +However, as we continue to develop PyScript it _is_ possible to get our latest +development version of PyScript via `npm` and we could (should there be enough +interest) deliver our work-in-progress via a CDN's "canary" or "development" +channel. **We do not guarantee the stability of such versions of PyScript**, +so never use them in production, and our documentation may not reflect the +development version. + +If you require the development version of PyScript, these are the URLs to use: + +```html title="PyScript development. ⚠️⚠️⚠️ WARNING: HANDLE WITH CARE! ⚠️⚠️⚠️" + + +``` + +!!! warning + + ***Do not use shorter urls or other CDNs.*** + + PyScript needs both the correct headers to use workers and to find its own + assets at runtime. Other CDN links might result into a **broken + experience**. + +### Workers via JavaScript + +Sometimes you want to start a Pyodide or MicroPython web worker from +JavaScript. + +Here's how: + +```html title="Starting a PyScript worker from JavaScript." + +``` + +```python title="micro.py" +from pyscript import sync + +def do_stuff(): + print("heavy computation") + +# Note: this reference is awaited in the JavaScript code. +sync.doStuff = do_stuff +``` + +### JavaScript `Class.new()` + +When using Python to instantiate a class defined in JavaScript, one needs to +use the class's `new()` method, rather than just using `Class()` (as in +Python). + +Why? + +The reason is technical, related to JavaScript's history and its relatively +poor introspection capabilities: + +* In JavaScript, `typeof function () {}` and `typeof class {}` produce the + same outcome: `function`. This makes it **very hard to disambiguate the + intent of the caller** as both are valid, JavaScript used to use + `function` (rather than `class`) to instantiate objects, and the class you're + using may not use the modern, `class` based, idiom. +* In the FFI, the JavaScript proxy has traps to intercept the use of the + `apply` and `construct` methods used during instantiation. However, because + of the previous point, it's not possible to be sure that `apply` is meant to + `construct` an instance or call a function. +* Unlike Python, just invoking a `Class()` in JavaScript (without + [the `new` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new)) + throws an error. +* Using `new Class()` is invalid syntax in Python. So there is still a need to + somehow disambiguate the intent to call a function or instantiate a class. +* Making use of the capitalized-name-for-classes convention is brittle because + when JavaScript code is minified the class name can sometimes change. +* This leaves our convention of `Class.new()` to explicitly signal the intent + to instantiate a JavaScript class. While not ideal it is clear and + unambiguous. + +### PyScript events + +PyScript uses hooks during the lifecycle of the application to facilitate the +[creation of plugins](../user-guide/plugins/). + +Beside hooks, PyScript also dispatches events at specific moments in the +lifecycle of the app, so users can react to changes in state: + +#### m/py:ready + +Both the `mpy:ready` and `py:ready` events are dispatched for every PyScript +related element found on the page. This includes ` + +
bootstrapping
+ +``` + +A classic use case for this event is to recreate the "starting up" +spinner that used to be displayed when PyScript bootstrapped. Just show the +spinner first, then close it once `py:ready` is triggered! + +!!! warning + + If using Pyodide on the main thread, the UI will block until Pyodide has + finished bootstrapping. The "starting up" spinner won't work unless Pyodide + is started on a worker instead. + +#### m/py:done + +The `mpy:done` and `py:done` events dispatch after the either the synchronous +or asynchronous code has finished execution. + +```html title="A py:done example." + + + +
bootstrapping
+ +``` + +!!! warning + + If `async` code contains an infinite loop or some orchestration that keeps + it running forever, then these events may never trigger because the code + never really finishes. + +#### py:all-done + +The `py:all-done` event dispatches when all code is finished executing. + +This event is special because it depends upon all the MicroPython and Pyodide +scripts found on the page, no matter the interpreter. + +In this example, MicroPython waves before Pyodide before the `"everything is +done"` message is written to the browser's console. + +```html title="A py:all-done example." + + + +``` + +#### m/py:progress + +The `py:progress` or `mpy:progress` event triggers on the main thread *during* +interpreter bootstrap (no matter if your code is running on main or in a +worker). + +The received `event.detail` is a string that indicates operations between +`Loading {what}` and `Loaded {what}`. So, the first event would be, for +example, `Loading Pyodide` and the last one per each bootstrap would be +`Loaded Pyodide`. + +In between all operations are `event.detail`s, such as: + + * `Loading files` and `Loaded files`, when `[files]` is found in the optional + config + * `Loading fetch` and `Loaded fetch`, when `[fetch]` is found in the optional + config + * `Loading JS modules` and `Loaded JS modules`, when `[js_modules.main]` or + `[js_modules.worker]` is found in the optional config + * finally, all optional packages handled via *micropip* or *mip* will also + trigger various `Loading ...` and `Loaded ...` events so that users can see + what is going on while PyScript is bootstrapping + +An example of this listener applied to a dialog can be [found in here](https://agiammarchi.pyscriptapps.com/kmeans-in-panel-copy/v1/). + +### Packaging pointers + +Applications need third party packages and [PyScript can be configured to +automatically install packages for you](user-guide/configuration/#packages). +Yet [packaging can be a complicated beast](#python-packages), so here are some +hints for a painless packaging experience with PyScript. + +There are essentially five ways in which a third party package can become +available in PyScript. + +1. The module is already part of either the Pyodide or MicroPython + distribution. For instance, Pyodide includes numpy, pandas, scipy, + matplotlib and scikit-learn as pre-built packages you need only activate + via the [`packages` setting](../user-guide/configuration/#packages) in + PyScript. There are plans for MicroPython to offer different builds for + PyScript, some to include MicroPython's version of numpy or the API for + sqlite. +2. Host a standard Python package somewhere (such as + [PyScript.com](https://pyscript.com) or in a GitHub repository) so it can + be fetched as a package via a URL at runtime. +3. Reference hosted Python source files, to be included on the file + system, via the [`files` setting](../user-guide/configuration/#files). +4. Create a folder containing the package's files and sub folders, and create + a hosted `.zip` or `.tgz`/`.tar.gz`/`.whl` archive to be decompressed into + the file system (again, via the + [`files` setting](../user-guide/configuration/#files)). +5. Provide your own `.whl` package and reference it via a URL in the + `packages = [...]` list. + +#### Host a package + +Just put the package you need somewhere it can be served (like +[PyScript.com](https://pyscript.com/)) and reference the URL in the +[`packages` setting](../user-guide/configuration/#packages). So long as the +server at which you are hosting the package +[allows CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) +(fetching files from other domains) everything should just work. + +It is even possible to install such packages at runtime, as this example using +MicroPython's [`mip` tool](https://docs.micropython.org/en/latest/reference/packages.html) +demonstrates (the equivalent can be achieved with Pyodide +[via `micropip`](https://micropip.pyodide.org/en/stable/)). + +```python title="MicroPython mip example." +# Install default version from micropython-lib +mip.install("keyword") + +# Install from raw URL +mip.install("https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/bisect/bisect.py") + +# Install from GitHub shortcut +mip.install("github:jeffersglass/some-project/foo.py") +``` + +#### Provide your own file + +One can use the [`files` setting](../user-guide/configuration/#files) to copy +packages onto the Python path: + +```html title="A file copied into the Python path." + +[files] +"./modules/bisect.py" = "./bisect.py" + + +``` + +#### Code archive (`zip`/`tgz`/`whl`) + +Compress all the code you want into an archive (using either either `zip` or +`tgz`/`tar.gz`). Host the resulting archive and use the +[`files` setting](../user-guide/configuration/#files) to decompress it onto +the Python interpreter's file system. + +Consider the following file structure: + +``` +my_module/__init__.py +my_module/util.py +my_module/sub/sub_util.py +``` + +Host it somewhere, and decompress it into the home directory of the Python +interpreter: + +```html title="A code archive." + +[files] +"./my_module.zip" = "./*" + + + +``` + +Please note, the target folder must end with a star (`*`), and will contain +everything in the archive. For example, `"./*"` refers to the home folder for +the interpreter. + +### File System + +Python expects a file system. In PyScript each interpreter provides its own +in-memory **virtual** file system. **This is not the same as the filesystem +on the user's device**, but is simply a block of memory in the browser. + +!!! warning + + **The file system is not persistent nor shareable** (yet). + + Every time a user loads or stores files, it is done in ephemeral memory + associated with the current browser session. Beyond the life of the + session, nothing is shared, nothing is stored, nothing persists! + +#### Read/Write + +The easiest way to add content to the virtual file system is by using native +Python file operations: + +```python title="Writing to a text file." +with open("./test.txt", "w") as dest: + dest.write("hello vFS") + dest.close() + +# Read and print the written content. +with open("./test.txt", "r") as f: + content = f.read() + print(content) +``` + +Combined with our `pyscript.fetch` utility, it's also possible to store more +complex data from the web. + +```python title="Writing a binary file." +# Assume async execution. +from pyscript import fetch, window + +href = window.location.href + +with open("./page.html", "wb") as dest: + dest.write(await fetch(href).bytearray()) + +# Read and print the current HTML page. +with open("./page.html", "r") as source: + print(source.read()) +``` + +#### Upload + +It's possible to upload a file onto the virtual file system from the browser +(``), and using the DOM API. + +The following fragment is just one way to achieve this. It's very simple and +builds on the file system examples already seen. + +```html title="Upload files onto the virtual file system via the browser." + + + + + +``` + +#### Download + +It is also possible to create a temporary link through which you can download +files present on the interpreter's virtual file system. + + +```python title="Download file from the virtual file system." +from pyscript import document, ffi, window +import os + + +def download_file(path, mime_type): + name = os.path.basename(path) + with open(path, "rb") as source: + data = source.read() + + # Populate the buffer. + buffer = window.Uint8Array.new(len(data)) + for pos, b in enumerate(data): + buffer[pos] = b + details = ffi.to_js({"type": mime_type}) + + # This is JS specific + file = window.File.new([buffer], name, details) + tmp = window.URL.createObjectURL(file) + dest = document.createElement("a") + dest.setAttribute("download", name) + dest.setAttribute("href", tmp) + dest.click() + + # here a timeout to window.URL.revokeObjectURL(tmp) + # should keep the memory clear for the session +``` + +### create_proxy + +The `create_proxy` function is described in great detail +[on the FFI page](../user-guide/ffi/), but it's also useful to explain _when_ +`create_proxy` is needed and the subtle differences between Pyodide and +MicroPython. + +#### Background + +To call a Python function from JavaScript, the native Python function needs +to be wrapped in a JavaScript object that JavaScript can use. This JavaScript +object converts and normalises arguments passed into the function before +handing off to the native Python function. It also reverses this process with +any results from the Python function, and so converts and normalises values +before returning the result to JavaScript. + +The JavaScript primitive used for this purpose is the +[Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). +It enables "traps", such as +[apply](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply), +so the extra work required to call the Python function can happen. + +Once the `apply(target, self, args)` trap is invoked: + +* JavaScript must find the correct Python interpreter to evaluate the code. +* In JavaScript, the `self` argument for `apply` is probably ignored for most + common cases. +* All the `args` must be resolved and converted into their Python primitive + representations or associated Python objects. + +Ultimately, the targets referenced in the `apply` **must exist** in the Python +context so they are ready when the JavaScript `apply` method calls into the +Python context. + +**Here's the important caveat**: locally scoped Python functions, or functions +created at run time cannot be retained forever. + +```python title="A basic Python to JavaScript callback." +import js + +js.addEventListener( + "custom:event", + lambda e: print(e.type) +) +``` + +In this example, the anonymous `lambda` function has no reference in the Python +context. It's just delegated to the JavaScript runtime via `addEventListener`, +and then Python immediately garbage collects it. However, as previously +mentioned, such a Python object must exist for when the `custom:event` is +dispatched. + +Furthermore, there is no way to define how long the `lambda` should be kept +alive in the Python environment, nor any way to discover if the `custom:event` +callback will ever dispatch (so the `lambda` is forever pending). PyScript, the +browser and the Python interpreters can only work within a finite amount of +memory, so memory management and the "aliveness" of objects is important. + +Therefore, `create_proxy` is provided to delegate responsibility for the +lifecycle of an object to the author of the code. In other words, wrapping the +`lambda` in a call to `create_proxy` would ensure the Python interpreter +retains a reference to the anonymous function for future use. + +!!! info + + This probably feels strange! An implementation detail of how the Python + and JavaScript worlds interact with each other is bleeding into your code + via `create_proxy`. Surely, if we always just need to create a proxy, a + more elegant solution would be to do this automatically? + + As you'll see, this is a complicated situation with inevitable tradeoffs, + but ultimately, through the + [`experimental_create_proxy = "auto"` flag](../user-guide/configuration/#experimental_create_proxy), + you probably never need to use `create_proxy`. This section of + our docs gives you the context you need to make an informed decision. + +**However**, this isn't the end of the story. + +When a Python callback is attached to a specific JavaScript +instance (rather than passed as argument into an event listener), it is easy +for the Python interpreter to know when the function could be freed from the +memory. + +```python title="A sticky lambda." +from pyscript import document + +# logs "click" if nothing else stopped propagation +document.onclick = lambda e: print(e.type) +``` + +"**Wait, wat? This doesn't make sense at all!?!?**", is a valid +question/response to this situation. + +In this case there's +no need to use `create_proxy` because the JavaScript reference to which the +function is attached isn't going away and the interpreter can use the +[`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) +to destroy the `lambda` (or decrease its reference count) when the underlying +JavaScript reference to which it is attached is itself destroyed. + +#### In Pyodide + +The `create_proxy` utility was created +([among others](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#module-pyodide.ffi.wrappers)) +to smooth out and circumvent the afore mentioned memory issues when using +Python callables with JavaScript event handlers. + +Using it requires special care. The coder must invoke the `destroy()` method +when the Python callback is no longer needed. It means coders must track the +callback's lifecycle. But this is not always possible: + +* If the callback is passed into opaque third party libraries, the reference is + "lost in a limbo" where who-knows-when the reference should be freed. +* If the callback is passed to listeners, timers or promises it's hard to + predict when the callback is no longer needed. + +Luckily the `Promise` use case is automatically handled by Pyodide, but we're +still left with the other cases: + +```python title="Different Pyodide create_proxy contexts." +from pyscript import ffi, window + +# The create_proxy is needed when a Python +# function isn't attached to an object reference +# (but is, rather, an argument passed into +# the JavaScript context). + +# This is needed so a proxy is created for +# future use, even if `print` won't ever need +# to be freed from the Python runtime. +window.setTimeout( + ffi.create_proxy(print), + 100, + "print" +) + +# This is needed because the lambda is +# immediately garbage collected. +window.setTimeout( + ffi.create_proxy( + lambda x: print(x) + ), + 100, + "lambda" +) + +def print_type(event): + print(event.type) + +# This is needed even if `print_type` +# is not a scoped / local function. +window.addEventListener( + "some:event", + ffi.create_proxy(print_type), + # despite this intent, the proxy + # will be trapped forever if not destroyed + ffi.to_js({"once": True}) +) + +# This does NOT need create_function as it is +# attached to an object reference, hence observed to free. +window.Object().no_create_function = lambda: print("ok") +``` + +To simplify this complicated situation PyScript has an +`experimental_create_proxy = "auto"` flag. When set, **PyScript intercepts +JavaScript callback invocations, such as those in the example code above, and +automatically proxies and destroys any references that are garbage collected +in the JavaScript environment**. + +**When this flag is set to `auto` in your configuration, you should never need +to use `create_proxy` with Pyodide**. + +!!! Note + + When it comes code running on a web worker, due to the way browser work, no + Proxy can survive a round trip to the main thread and back. + + In this scenario PyScript works differently and references callbacks + via a unique id, rather than by their identity on the worker. When running + on a web worker, PyScript automatically frees proxy object references, so + you never need to use `create_proxy` when running code on a web worker. + +#### In MicroPython + +The proxy situation is definitely simpler in MicroPython. It just creates +proxies automatically (so there is no need for a manual `create_proxy` step). + +This is because MicroPython doesn't (yet) have a `destroy()` method for +proxies, rendering the use case of `create_proxy` redundant. + +Accordingly, **the use of `create_proxy` in MicroPython is only needed for +code portability purposes** between Pyodide and MicroPython. When using +`create_proxy` in MicroPython, it's just a pass-through function and doesn't +actually do anything. + +All the examples that require `create_proxy` in Pyodide, don't need it in +MicroPython: + +```python title="Different MicroPython create_proxy contexts." +from pyscript import window + +# This just works. +window.setTimeout(print, 100, "print") + +# This also just works. +window.setTimeout(lambda x: print(x), 100, "lambda") + +def print_type(event): + print(event.type) + +# This just works too. +window.addEventListener( + "some:event", + print_type, + ffi.to_js({"once": True}) +) + +# And so does this. +window.Object().no_create_function = lambda: print("ok") +``` + +### to_js + +Use of the `pyodide.ffi.to_js` function is described +[in the ffi page](../user-guide/ffi/#to_js). +But it's also useful to cover the *when* and *why* `to_js` is needed, if at +all. + +#### Background + +Despite their apparent similarity, +[Python dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) +and +[JavaScript object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) +are very different primitives: + +```python title="A Python dictionary." +ref = {"some": "thing"} + +# Keys don't need quoting, but only when initialising a dict... +ref = dict(some="thing") +``` + +```js title="A JavaScript object literal." +const ref = {"some": "thing"}; + +// Keys don't need quoting, so this is as equally valid... +const ref = {some: "thing"}; +``` + +In both worlds, accessing `ref["some"]` would produce the same result: the +string `"thing"`. + +However, in JavaScript `ref.some` (i.e. a dotted reference to the key) would +also work to return the string `"thing"` (this is not the case in Python), +while in Python `ref.get("some")` achieves the same result (and this is not the +case in JavaScript). + +Perhaps because of this, Pyodide chose to convert Python dictionaries to +JavaScript +[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) +objects that share a +[`.get` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) +with Python. + +Unfortunately, in idiomatic JavaScript and for the vast majority of APIs, +an object literal (rather than a `Map`) is used to represent key/value pairs. +Feedback from our users indicates the dissonance of using a `Map` rather than +the expected object literal to represent a Python `dict` is the source of a +huge amount of frustration. Sadly, the APIs for `Map` and object literals +are sufficiently different that one cannot be a drop in replacement for +another. + +Pyodide have provided a way to override the default `Map` based behaviour, but +this results some rather esoteric code: + +```python title="Convert a dict to an object literal in Pyodide." +import js +from pyodide.ffi import to_js + +js.callback( + to_js( + {"async": False}, + # Transform the default Map into an object literal. + dict_converter=js.Object.fromEntries + ) +) +``` +!!! info + + Thanks to a + [recent change in Pyodide](https://github.com/pyodide/pyodide/pull/4576), + such `Map` instances are + [duck-typed](https://en.wikipedia.org/wiki/Duck_typing) to behave like + object literals. Conversion may not be needed anymore, and `to_js` may just + work without the need of the `dict_converter`. Please check. + +MicroPython's version of `to_js` takes the opposite approach (for +many of the reasons stated above) and converts Python dictionaries to object +literals instead of `Map` objects. + +As a result, **the PyScript `pyscript.ffi.to_js` ALWAYS returns a JavaScript +object literal by default when converting a Python dictionary** no matter if +you're using Pyodide or MicroPython as your interpreter. Furthermore, when +using MicroPython, because things are closer to idiomatic JavaScript behaviour, +you may not even need to use `to_js` unless you want to ensure +cross-interpreter compatibility. + +#### Caveat + +!!! warning + + **When using `pyscript.to_js`, the result is detached from the original + Python dictionary.** + +Any change to the JavaScript object **will not be reflected in the original +Python object**. For the vast majority of use cases, this is a desirable +trade-off. But it's important to note this detachment. + +If you're simply passing data around, `pyscript.ffi.to_js` will fulfil your +requirements in a simple and idiomatic manner. diff --git a/docs/index.md b/docs/index.md index 97c97b2..2cfbb01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ ![PyScript Logo](assets/images/pyscript.svg) -

A platform for Python in the browser.

+

PyScript is an open source platform for Python in the browser.

## PyScript is... @@ -13,8 +13,8 @@ * **Shareable**: applications are just a URL on the web. That's it! * **Universal**: your code runs anywhere a browser runs... which is _everywhere_! -* **Secure**: PyScript runs in the world's most battle-tested secure platform, - the browser! +* **Secure**: PyScript runs in the world's most battle-tested computing + platform, the browser! * **Powerful**: the best of the web and Python, together at last. ## What's next? @@ -22,27 +22,43 @@
I'm a beginner...
Welcome! PyScript is designed to be friendly for beginner coders. The - best place to start is by reading our + best way to start is to read our beginning PyScript guide - and then using + and then use pyscript.com - to create your first apps.
+ to create your first apps. Problems? Check out our + frequently asked questions.
I'm already technical...
The beginner docs will set you up with a simple coding environment. For more in-depth technical coverage of PyScript, consult the - user guide.
+ user guide. The + example applications demonstrate many of the features + of PyScript. The API docs and FAQ + contain lots of technical detail. +
I want to contribute...
+
+

Welcome, friend! + PyScript is an open source project, we expect + participants to act in the spirit of our + code of conduct and we have many + ways in which you can contribute. + Our developer guide explains how to set + up a working development environment for PyScript.

+
Just show me...
That's easy! Just take a look around - pyscript.com.
-
I want more support...
+ pyscript.com - our + platform for developing and hosting PyScript applications. By using + using this service you help to support and sustain the development and growth + of the open-source PyScript project. +
I want support...
-

Join in with the conversation on our +

Join the conversation on our discord server, - for realtime chat with core maintainers and fellow users of PyScript.

-

Find out more about the open source project, who is involved and how - you could contribute or support our efforts via the - contributor's guide.

-

Explore + for realtime chat with core maintainers and fellow users of PyScript. + Check out our YouTube + channel, full of community calls and show and tells. + Explore educational and commercial diff --git a/docs/mini-coi.js b/docs/mini-coi.js new file mode 100644 index 0000000..b7a23bf --- /dev/null +++ b/docs/mini-coi.js @@ -0,0 +1,28 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +/*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */ +(({ document: d, navigator: { serviceWorker: s } }) => { + if (d) { + const { currentScript: c } = d; + s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => { + r.addEventListener('updatefound', () => location.reload()); + if (r.active && !s.controller) location.reload(); + }); + } + else { + addEventListener('install', () => skipWaiting()); + addEventListener('activate', e => e.waitUntil(clients.claim())); + addEventListener('fetch', e => { + const { request: r } = e; + if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return; + e.respondWith(fetch(r).then(r => { + const { body, status, statusText } = r; + if (!status || status > 399) return r; + const h = new Headers(r.headers); + h.set('Cross-Origin-Opener-Policy', 'same-origin'); + h.set('Cross-Origin-Embedder-Policy', 'require-corp'); + h.set('Cross-Origin-Resource-Policy', 'cross-origin'); + return new Response(body, { status, statusText, headers: h }); + })); + }); + } +})(self); diff --git a/docs/user-guide/architecture.md b/docs/user-guide/architecture.md new file mode 100644 index 0000000..be311b6 --- /dev/null +++ b/docs/user-guide/architecture.md @@ -0,0 +1,301 @@ +# Architecture, Lifecycle & Interpreters + +## Core concepts + +PyScript's architecture has three core concepts: + +1. A small, efficient and powerful kernel called + [PolyScript](https://github.com/pyscript/polyscript) is the foundation + upon which PyScript and plugins are built. +2. A library called [coincident](https://github.com/WebReflection/coincident#readme) + that simplifies and coordinates interactions with web workers. +3. The PyScript [stack](https://en.wikipedia.org/wiki/Solution_stack) inside + the browser is simple and clearly defined. + +### PolyScript + +[PolyScript](https://github.com/pyscript/polyscript) is the core of PyScript. + +!!! danger + + Unless you are an advanced user, you only need to know that PolyScript + exists, and it can be safely ignored. + +PolyScript's purpose is to bootstrap the platform and provide all the necessary +core capabilities. Setting aside PyScript for a moment, to use +*just PolyScript* requires a ` + + + + + + +``` + +!!! warning + + **PolyScript is not PyScript.** + + PyScript enhances the available Python interpreters with convenient + features, helper functions and easy-to-use yet powerful capabilities. + + These enhancements are missing from PolyScript. + +PolyScript's capabilities, upon which PyScript is built, can be summarised as: + +* Evaluation of code via [` +``` + +If you use JSON, you can make it the value of the `config` attribute: + +```HTML title="JSON as the value of the config attribute." + +``` + +For historical and convenience reasons we still support the inline +specification of configuration information for `py` and `mpy` type scripts via a +_single_ `` or `` tag in your HTML document: + +```HTML title="Inline configuration via the <py-config> tag." + +{ + "packages": ["arrr", "numberwang" ] +} + +``` + +!!! warning + + Should you use `` or ``, **there must be only one of + these tags on the page per interpreter**. + + Additionally, `` only works for `py`/`mpy` type scripts and is not used + with [`py-game`](../pygame-ce) or [`py-editor`](../editor). For these use the config + attribute method. + +## Options + +There are five core options ([`interpreter`](#interpreter), [`files`](#files), +[`packages`](#packages), [`js_modules`](#javascript-modules) and +[`sync_main_only`](#sync_main_only)) and an experimental flag +([`experimental_create_proxy`](#experimental_create_proxy)) that can be used in +the configuration of PyScript. The user is also free to define +arbitrary additional configuration options that plugins or an app may require +for their own reasons. + +### Interpreter + +The `interpreter` option pins the Python interpreter to the version of the +specified value. This is useful for testing (does my code work on a specific +version of Pyodide?), or to ensure the precise combination of PyScript version +and interpreter version are pinned to known values. + +The value of the `interpreter` option should be a valid version number +for the Python interpreter you are configuring, or a fully qualified URL to +a custom version of the interpreter. + +The following two examples are equivalent: + +```TOML title="Specify the interpreter version in TOML." +interpreter = "0.23.4" +``` + +```JSON title="Specify the interpreter version in JSON." +{ + "interpreter": "0.23.4" +} +``` + +The following JSON fragment uses a fully qualified URL to point to the same +version of Pyodide as specified in the previous examples: + +```JSON title="Specify the interpreter via a fully qualified URL." +{ + "interpreter": "https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.mjs" +} +``` + +### Files + +The `files` option fetches arbitrary content from URLs onto the virtual +filesystem available to Python, and emulated by the browser. Just map a valid +URL to a destination filesystem path on the in-browser virtual filesystem. You +can find out more in the section about +[PyScript and filesystems](../filesystem/). + +The following JSON and TOML are equivalent: + +```json title="Fetch files onto the filesystem with JSON." +{ + "files": { + "https://example.com/data.csv": "./data.csv", + "./code.py": "./subdir/code.py" + } +} +``` + +```toml title="Fetch files onto the filesystem with TOML." +[files] +"https://example.com/data.csv" = "./data.csv" +"./code.py" = "./subdir/code.py" +``` + +If you make the target an empty string, the final "filename" part of the source +URL becomes the destination filename, in the root of the filesystem, to which +the content is copied. As a result, the `data.csv` entry from the previous +examples could be equivalently re-written as: + +```json title="JSON implied filename in the root directory." +{ + "files": { + "https://example.com/data.csv": "", + "./code.py": "" + } +} +``` + +```toml title="TOML implied filename in the root directory." +[files] +"https://example.com/data.csv" = "" +"./code.py" = "" +``` + +If the source part of the configuration is either a `.zip` or `.tar.gz` file +and its destination is a folder path followed by a star (e.g. `/*` or +`./dest/*`), then PyScript will extract the referenced archive automatically +into the target directory in the browser's built in file system. + +!!! warning + + **PyScript expects all file destinations to be unique.** + + If there is a duplication PyScript will raise an exception to help you find + the problem. + +!!! warning + **Use destination URLs instead of CORS / redirect URLs.** + + For example, `https://github.com/pyscript/ltk/raw/refs/heads/main/ltk/jquery.py` + redirects to `https://raw.githubusercontent.com/pyscript/ltk/refs/heads/main/ltk/jquery.py`. Use the latter. + +!!! tip + + **For most people, most of the time, the simple URL to filename mapping, + described above, will be sufficient.** + + Yet certain situations may require more flexibility. In which case, read + on. + +Sometimes many resources are needed to be fetched from a single location and +copied into the same directory on the file system. To aid readability and +reduce repetition, the `files` option comes with a mini +[templating language](https://en.wikipedia.org/wiki/Template_processor) +that allows reusable placeholders to be defined between curly brackets (`{` +and `}`). When these placeholders are encountered in the `files` configuration, +their name is replaced with their associated value. + +!!! Attention + + Valid placeholder names are always enclosed between curly brackets + (`{` and `}`), like this: `{FROM}`, `{TO}` and `{DATA SOURCE}` + (capitalized names help identify placeholders + when reading code ~ although this isn't strictly necessary). + + Any number of placeholders can be defined and used anywhere within URLs and + paths that map source to destination. + +The following JSON and TOML are equivalent: + +```json title="Using the template language in JSON." +{ + "files": { + "{DOMAIN}": "https://my-server.com", + "{PATH}": "a/path", + "{VERSION}": "1.2.3", + "{FROM}": "{DOMAIN}/{PATH}/{VERSION}", + "{TO}": "./my_module", + "{FROM}/__init__.py": "{TO}/__init__.py", + "{FROM}/foo.py": "{TO}/foo.py", + "{FROM}/bar.py": "{TO}/bar.py", + "{FROM}/baz.py": "{TO}/baz.py", + } +} +``` + +```toml title="Using the template language in TOML." +[files] +"{DOMAIN}" = "https://my-server.com" +"{PATH}" = "a/path" +"{VERSION}" = "1.2.3" +"{FROM}" = "{DOMAIN}/{PATH}/{VERSION}" +"{TO}" = "./my_module" +"{FROM}/__init__.py" = "{TO}/__init__.py" +"{FROM}/foo.py" = "{TO}/foo.py" +"{FROM}/bar.py" = "{TO}/bar.py" +"{FROM}/baz.py" = "{TO}/baz.py" +``` + +The `{DOMAIN}`, `{PATH}`, and `{VERSION}` placeholders are +used to create a further `{FROM}` placeholder. The `{TO}` placeholder is also +defined to point to a common sub-directory on the file system. The final four +entries use `{FROM}` and `{TO}` to copy over four files (`__init__.py`, +`foo.py`, `bar.py` and `baz.py`) from the same source to a common destination +directory. + +For convenience, if the destination is just a directory (it ends with `/`) +then PyScript automatically uses the filename part of the source URL as the +filename in the destination directory. + +For example, the end of the previous config file could be: + +```toml +"{TO}" = "./my_module/" +"{FROM}/__init__.py" = "{TO}" +"{FROM}/foo.py" = "{TO}" +"{FROM}/bar.py" = "{TO}" +"{FROM}/baz.py" = "{TO}" +``` + +### Packages + +The `packages` option lists +[Python packages](https://packaging.python.org/en/latest/) +to be installed onto the Python path. + +!!! info + + Pyodide uses a + [utility called `micropip`](https://micropip.pyodide.org/en/stable/index.html) + to install packages [from PyPI](https://pypi.org/). + + Because `micropip` is a Pyodide-only feature, and MicroPython doesn't + support code packaged on PyPI, **the `packages` option only works with + packages hosted on PyPI when using Pyodide**. + + MicroPython's equivalent utility, + [`mip`](https://docs.micropython.org/en/latest/reference/packages.html), + **uses a separate repository of available packages called + [`micropython-lib`](https://github.com/micropython/micropython-lib)**. + When you use the `packages` option with MicroPython, it is this repository + (not PyPI) that is used to find available packages. Many of the packages + in `micropython-lib` are for microcontroller based activities and + **may not work with the web assembly port** of MicroPython. + + If you need **pure Python modules for MicroPython**, you have two further + options: + + 1. Use the [files](#files) option to manually copy the source code for a + package onto the file system. + 2. Use a URL referencing a MicroPython friendly package instead of PyPI + package name. + +The following two examples are equivalent: + +```TOML title="A packages list in TOML." +packages = ["arrr", "numberwang", "snowballstemmer>=2.2.0" ] +``` + +```JSON title="A packages list in JSON." +{ + "packages": ["arrr", "numberwang", "snowballstemmer>=2.2.0" ] +} +``` + +When using Pyodide, the names in the list of `packages` can be any of the +following valid forms: + +* A name of a package on PyPI: `"snowballstemmer"` +* A name for a package on PyPI with additional constraints: + `"snowballstemmer>=2.2.0"` +* An arbitrary URL to a Python package: `"https://.../package.whl"` +* A file copied onto the browser based file system: `"emfs://.../package.whl"` + +#### Package Cache + +For performance reasons, PyScript caches packages so that a delay resulting +from downloading the packages is only noticable on first load - after which, +PyScript will fall back on packages previously downloaded and held in the +browser's local cache. + +The behaviour of caching can be configured via the `packages_cache` setting. If +this setting is not used, PyScript will cache packages. Otherwise, override +PyScript's behaviour by setting `packages_cache` to one of these two values: + +* `never` - PyScript will not cache packages. +* `passthrough` - this only works with Pyodide (see [this wiki](https://deepwiki.com/cloudflare/pyodide/3-package-system)), + and will cause Pyodide to download packages in a parallel manner rather than + the default linear fashion. However, these packages will not be cached. + +### Plugins + +The `plugins` option allows user to either augment, or exclude, the list of +plugins imported out of the box from *core* during bootstrap. + +While augmenting requires some knowledge about *core* internals, excluding +some plugin might be desired to avoid such plugin behavior and, in edge cases, +reduce the amount of network requests to bootstrap *PyScript*. + +It is possible to check the [list of plugins](https://github.com/pyscript/pyscript/blob/main/core/src/plugins.js) +we offer by default, where each *key* is used as plugin name and could be also +disabled using the `!pugin-name` convention, here an example: + +```TOML title="Specify plugins in TOML" +plugins = ["custom_plugin", "!error"] +``` + +```JSON title="Specify plugins in JSON" +{ + "plugins": ["custom_plugin", "!error"] +} +``` + +!!! info + + The `"!error"` syntax is a way to turn off a plugin built into PyScript + that is enabled by default. + + It is possible to turn off other plugins too using the very same + convention. + +!!! warning + + Please note `plugins` are currently a *core* only feature. If you need any + extra functionality out of the box *files* or *js_modules* are the current + way to provide more features without needing to file a *PR* in *core*. + + This means that the current `plugins` proposal is meant to disable our own + plugins but it has no usage to add 3rd party plugins right now. + +### JavaScript modules + +It's easy to import and use JavaScript modules in your Python code. This +section of the docs examines the configuration needed to make this work. How +to make use of JavaScript is dealt with +[elsewhere](../dom/#working-with-javascript). + +We need to tell PyScript about the JavaScript modules you want to +use. This is the purpose of the `js_modules` related configuration fields. + +There are two fields: + +* `js_modules.main` defines JavaScript modules loaded in the context of the + main thread of the browser. Helpfully, it is also possible to interact with + such modules **from the context of workers**. Sometimes such modules also + need CSS files to work, and these can also be specified. +* `js_modules.worker` defines JavaScript modules loaded into the context of + the web worker. Such modules **must not expect** `document` or `window` + references (if this is the case, you must load them via `js_modules.main` and + use them from the worker). However, if the JavaScript module could work + without such references, then performance is better if defined on a worker. + Because CSS is meaningless in the context of a worker, it is not possible to + specify such files in a worker context. + +Once specified, your JavaScript modules will be available under the +`pyscript.js_modules.*` namespace. + +To specify such modules, simply provide a list of source/module name pairs. + +For example, to use the excellent [Leaflet](https://leafletjs.com/) JavaScript +module for creating interactive maps you'd add the following lines: + +```TOML title="JavaScript main thread modules defined in TOML." +[js_modules.main] +"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js" = "leaflet" +"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" = "leaflet" # CSS +``` + +```JSON title="JavaScript main thread modules defined in JSON." +{ + "js_modules": { + "main": { + "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js": "leaflet", + "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css": "leaflet" + } + } +} +``` + +!!! info + + Notice how the second line references the required CSS needed for the + JavaScript module to work correctly. + + The CSS file **MUST** target the very same name as the JavaScript module to + which it is related. + +!!! warning + + Since the Leaflet module expects to manipulate the DOM and have access to + `document` and `window` references, **it must only be added via the + `js_modules.main` setting** (as shown) and cannot be added in a worker + context. + +At this point Python code running on either the main thread or in a +worker will have access to the JavaScript module like this: + +```python title="Making use of a JavaScript module from within Python." +from pyscript.js_modules import leaflet as L + +map = L.map("map") + +# etc.... +``` + +Some JavaScript modules (such as +[html-escaper](https://www.npmjs.com/package/html-escaper)) don't require +access to the DOM and, for efficiency reasons, can be included in the worker +context: + +```TOML title="A JavaScript worker module defined in TOML." +[js_modules.worker] +"https://cdn.jsdelivr.net/npm/html-escaper" = "html_escaper" +``` + +```JSON title="A JavaScript worker module defined in JSON." +{ + "js_modules": { + "worker": { + "https://cdn.jsdelivr.net/npm/html-escaper": "html_escaper" + } + } +} +``` + +However, `from pyscript.js_modules import html_escaper` would then only work +within the context of Python code **running on a worker**. + +### sync_main_only + +Sometimes you just want to start an expensive computation on a web worker +without the need for the worker to interact with the main thread. You're simply +awaiting the result of a method exposed from a worker. + +This has the advantage of not requiring the use of `SharedArrayBuffer` and +[associated CORS related header configuration](../workers/#http-headers). + +If the `sync_main_only` flag is set, then **interactions between the main thread +and workers are limited to one way calls from the main thread to methods +exposed by the workers**. + +```TOML title="Setting the sync_main_only flag in TOML." +sync_main_only = true +``` + +```JSON title="Setting the sync_main_only flag in JSON." +{ + "sync_main_only": true +} +``` + +If `sync_main_only` is set, the following caveats apply: + +* It is not possible to manipulate the DOM or do anything meaningful on the + main thread **from a worker**. This is because Atomics cannot guarantee + sync-like locks between a worker and the main thread. +* Only a worker's `pyscript.sync` methods are exposed, and **they can only be + awaited from the main thread**. +* The worker can only `await` main thread references one after the other, so + developer experience is degraded when one needs to interact with the + main thread. + +### experimental_create_proxy + +Knowing when to use the `pyscript.ffi.create_proxy` method when using Pyodide +can be confusing at the best of times and full of +[technical "magic"](../ffi#create_proxy). + +This _experimental_ flag, when set to `"auto"` will cause PyScript to try to +automatically handle such situations, and should "just work". + +```TOML title="Using the experimental_create_proxy flag in TOML." +experimental_create_proxy = "auto" +``` + +```JSON title="Using the experimental_create_proxy flag in JSON." +{ + "experimental_create_proxy": "auto" +} +``` + +!!! warning + + **This feature is _experimental_ and only needs to be used with Pyodide.** + + Should you encounter problems (such as problematic memory leaks) when using + this flag with Pyodide, please don't hesitate to + [raise an issue](https://github.com/pyscript/pyscript/issues) with a + reproducable example, and we'll investigate. + +### experimental_ffi_timeout + +When bootstrapping a worker, the worker is told to use a cache for round-trip +operations (for example, `window.my_object.foo.bar.baz` causes a round-trip to +the main thread for each dot `.` in this chain of references). By caching the +dotted references performance can be improved by reducing the number of +round trips PyScript makes. + +However, not everything can be cached (those APIs or objects with side-effects +won't work; for example DOM element based APIs etc). + +The `experimental_ffi_timeout` setting defines the maximum lifetime of that +cache. If it's less than 0 (the default), there is no cache whatsoever. Zero +means to clean up the cache on the next iteration of the event loop. A positive +number is the maximum number of milliseconds the cache will be kept alive. + +In this experimental phase, we suggest trying `0`, `30` or a value that won't +likely bypass the browser rendering of 60fps. Of course, `1000` (i.e. a second) +would be a fun, if greedy, experiment. + +### debug + +When using Pyodide, if the `debug` setting is set to `true`, then Pyodide will +run in debug mode. See Pyodide's documentation for details of what this +entails. + +### Custom + +Sometimes plugins or apps need bespoke configuration options. + +So long as you don't cause a name collision with the built-in option names then +you are free to use any valid data structure that works with both TOML and JSON +to express your configuration needs. + +Access the current configuration via `pyscript.config`, a Python `dict` +representing the configuration: + +```python title="Reading the current configuration." +from pyscript import config + + +# It's just a dict. +print(config.get("files")) +``` + +!!! note + + Changing the `config` dictionary at runtime doesn't change the actual + configuration. diff --git a/docs/user-guide/dom.md b/docs/user-guide/dom.md index dc2117a..1859cb8 100644 --- a/docs/user-guide/dom.md +++ b/docs/user-guide/dom.md @@ -1,2 +1,493 @@ -# Working with the DOM +# The DOM +The DOM +([document object model](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model)) +is a tree like data structure representing the web page displayed by the +browser. PyScript interacts with the DOM to change the user interface and react +to things happening in the browser. + +There are currently two ways to interact with the DOM: + +1. Through the [foreign function interface](#ffi) (FFI) to interact with objects found + in the browser's `globalThis` or `document` objects. +2. Through the [`pydom` module](#pydom) that acts as a Pythonic wrapper around + the FFI and comes as standard with PyScript. + +## FFI + +The foreign function interface (FFI) gives Python access to all the +[standard web capabilities and features](https://developer.mozilla.org/en-US/docs/Web), +such as the browser's built-in +[web APIs](https://developer.mozilla.org/en-US/docs/Web/API). + +This is available via the `pyscript.window` module which is a proxy for +the main thread's `globalThis` object, or `pyscript.document` which is a proxy +for the website's `document` object in JavaScript: + +```Python title="Accessing the window and document objects in Python" +from pyscript import window, document + + +my_element = document.querySelector("#my-id") +my_element.innerText = window.location.hostname +``` + +The FFI creates _proxy objects_ in Python linked to _actual objects_ in +JavaScript. + +The proxy objects in your Python code look and behave like Python +objects but have related JavaScript objects associated with them. It means the +API defined in JavaScript remains the same in Python, so any +[browser based JavaScript APIs](https://developer.mozilla.org/en-US/docs/Web/API) +or third party JavaScript libraries that expose objects in the web page's +`globalThis`, will have exactly the same API in Python as in JavaScript. + +The FFI automatically transforms Python and JavaScript objects into the +equivalent in the other language. For example, Python's boolean `True` and +`False` will become JavaScript's `true` and `false`, while a JavaScript array +of strings and integers, `["hello", 1, 2, 3]` becomes a Python list of the +equivalent values: `["hello", 1, 2, 3]`. + +!!! info + + Instantiating classes into objects is an interesting special case that the + FFI expects you to handle. + + **If you wish to instantiate a JavaScript class in your Python + code, you need to call the class's `new` method:** + + ```python + from pyscript import window + + + my_obj = window.MyJavaScriptClass.new("some value") + + ``` + + The underlying reason for this is simply JavaScript and Python do + instantiation very differently. By explicitly calling the JavaScript + class's `new` method PyScript both signals and honours this difference. + + More technical information about instantiating JavaScript classes can be + [found in the FAQ](../../faq/#javascript-classnew) + +Should you require lower level API access to FFI features, you can find such +builtin functions under the `pyscript.ffi` namespace in both Pyodide and +MicroPython. The available functions are described in our section on the +[builtin API](../../api). + +Advanced users may wish to explore the +[technical details of the FFI](../ffi). + +## `pyscript.web` + +!!! warning + + The `pyscript.web` module is currently a work in progress. + + We welcome feedback and suggestions. + +The `pyscript.web` module is an idiomatically Pythonic API for interacting with +the DOM. It wraps the FFI in a way that is more familiar to Python developers +and works natively with the Python language. Technical documentation for this +module can be found in [the API](../../api/#pyscriptweb) section. + +There are three core concepts to remember: + +* Find elements on the page via + [CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors). + The `find` API uses exactly the [same queries](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Locating_DOM_elements_using_selectors) + as those used by native browser methods like `qurerySelector` or + `querySelectorAll`. +* Use classes in the `pyscript.web` namespace to create and organise + new elements on the web page. +* Collections of elements allow you to access and change attributes en-mass. + Such collections are returned from `find` queries and are also used for the + [children](https://developer.mozilla.org/en-US/docs/Web/API/Element/children) + of an element. + +You have several options for accessing the content of the page, and these are +all found in the `pyscript.web.page` object. The `html`, `head` and `body` +attributes reference the page's top-level html, head and body. As a convenience +the `page`'s `title` attribute can be used to get and set the web page's title +(usually shown in the browser's tab). The `append` method is a shortcut for +adding content to the page's `body`. Whereas, the `find` method is used to +return collections of elements matching a CSS query. You may also shortcut +`find` via a CSS query in square brackets. Finally, all elements have a `find` +method that searches within their children for elements matching your CSS +query. + +```python +from pyscript.web import page + + +# Print all the child elements of the document's head. +print(page.head.children) +# Find all the paragraphs in the DOM. +paragraphs = page.find("p") +# Or use square brackets. +paragraphs = page["p"] +``` + +The object returned from a query, or used as a reference to an element's +children is iterable: + +```python +from pyscript.web import page + + +# Get all the paragraphs in the DOM. +paragraphs = page["p"] + +# Print the inner html of each paragraph. +for p in paragraphs: + print(p.html) +``` + +Alternatively, it is also indexable / sliceable: + +```python +from pyscript.web import page + + +# Get an ElementCollection of all the paragraphs in the DOM +paragraphs = page["p"] + +# Only the final two paragraphs. +for p in paragraphs[-2:]: + print(p.html) +``` + +You have access to all the standard attributes related to HTML elements (for +example, the `innerHTML` or `value`), along with a couple of convenient ways +to interact with classes and CSS styles: + +* `classes` - the list of classes associated with the elements. +* `style` - a dictionary like object for interacting with CSS style rules. + +For example, to continue the example above, `paragraphs.innerHTML` will return +a list of all the values of the `innerHTML` attribute on each contained +element. Alternatively, set an attribute for all elements contained in the +collection like this: `paragraphs.style["background-color"] = "blue"`. + +It's possible to create new elements to add to the page: + +```python +from pyscript.web import page, div, select, option, button, span, br + + +page.append( + div( + div("Hello!", classes="a-css-class", id="hello"), + select( + option("apple", value=1), + option("pear", value=2), + option("orange", value=3), + ), + div( + button(span("Hello! "), span("World!"), id="my-button"), + br(), + button("Click me!"), + classes=["css-class1", "css-class2"], + style={"background-color": "red"} + ), + div( + children=[ + button( + children=[ + span("Hello! "), + span("Again!") + ], + id="another-button" + ), + br(), + button("b"), + ], + classes=["css-class1", "css-class2"] + ) + ) +) +``` + +This example demonstrates a declaritive way to add elements to the body of the +page. Notice how the first (unnamed) arguments to an element are its children. +The named arguments (such as `id`, `classes` and `style`) refer to attributes +of the underlying HTML element. If you'd rather be explicit about the children +of an element, you can always pass in a list of such elements as the named +`children` argument (you see this in the final `div` in the example above). + +Of course, you can achieve similar results in an imperative style of +programming: + +```python +from pyscript.web import page, div, p + + +my_div = div() +my_div.style["background-color"] = "red" +my_div.classes.add("a-css-class") + +my_p = p() +my_p.content = "This is a paragraph." + +my_div.append(my_p) + +# etc... +``` + +It's also important to note that the `pyscript.when` decorator understands +element references from `pyscript.web`: + +```python +from pyscript import when +from pyscript.web import page + + +btn = page["#my-button"] + + +@when("click", btn) +def my_button_click_handler(event): + print("The button has been clicked!") +``` + +Should you wish direct access to the proxy object representing the underlying +HTML element, each Python element has a `_dom_element` property for this +purpose. + +Once again, the technical details of these classes are described in the +[built-in API documentation](../../api/#pyscriptweb). + +## Working with JavaScript + +There are three ways in which JavaScript can get into a web page. + +1. As a global reference attached to the `window` object in the web page + because the code was referenced as the source of a `script` tag in your HTML + (the very old school way to do this). +2. Using the [Universal Module Definition](https://github.com/umdjs/umd) (UMD), + an out-of-date and non-standard way to create JavaScript modules. +3. As a standard + [JavaScript Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) + which is the modern, standards compliant way to define and use a JavaScript + module. If possible, this is the way you should do things. + +Sadly, this being the messy world of the web, methods 1 and 2 are still quite +common, and so you need to know about them so you're able to discern and work +around them. There's nothing WE can do about this situation, but we can +suggest "best practice" ways to work around each situation. + +Remember, as mentioned +[elsewhere in our documentation](../configuration/#javascript-modules), +the standard way to get JavaScript modules into your PyScript Python context is +to link a _source_ standard JavaScript module to a _destination_ name: + +```toml title="Reference a JavaScript module in the configuration." +[js_modules.main] +"https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js" = "leaflet" +``` + +Then, reference the module via the destination name in your Python code, by +importing it from the `pyscript.js_modules` namespace: + +```python title="Import the JavaScript module into Python" +from pyscript.js_modules import leaflet as L + +map = L.map("map") + +# etc.... +``` + +We'll deal with each of the potential JavaScript related situations in turn: + +### JavaScript as a global reference + +In this situation, you have some JavaScript code that just globally defines +"stuff" in the context of your web page via a `script` tag. Your HTML will +contain something like this: + +```html title="JavaScript as a global reference" + + + + + + +``` + +When you find yourself in this situation, simply use the `window` object in +your Python code (found in the `pyscript` namespace) to interact with the +resulting JavaScript objects: + +```python title="Python interaction with the JavaScript global reference" +from pyscript import window, document + + +# The window object is the global context of your web page. +html = window.html + +# Just use the object "as usual"... +# e.g. show escaped HTML in the body: <> +document.body.append(html.escape("<>")) +``` + +You can find an example of this technique here: + +[https://pyscript.com/@agiammarchi/floral-glade/v1](https://pyscript.com/@agiammarchi/floral-glade/v1) + +### JavaScript as a non-standard UMD module + +Sadly, these sorts of non-standard JavaScript modules are still quite +prevalent. But the good news is there are strategies you can use to help you +get them to work properly. + +The non-standard UMD approach tries to check for `export` and `module` fields +in the JavaScript module and, if it doesn’t find them, falls back to treating +the module in the same way as a global reference described above. + +If you find you have a UMD JavaScript module, there are services online to +automagically convert it to the modern and standards compliant way to d +o JavaScript modules. A common (and usually reliable) service is provided by +[https://esm.run/your-module-name](https://esm.run/your-module-name), a +service that provides an out of the box way to consume the module in the +correct and standard manner: + +```html title="Use esm.run to automatically convert a non-standard UMD module" + + +``` + +If a similar test works for the module you want to use, use the esm.run CDN +service within the `py` or `mpy` configuration file as explained at the start +of this section on JavaScript (i.e. you'll use it via the `pyscript.js_modules` +namespace). + +If this doesn't work, assume the module is not updated nor migrated to a state +that can be automatically translated by services like esm.run. You could try an +alternative (more modern) JavaScript module to achieve you ends or (if it +really must be this module), you can wrap it in a new JavaScript module that +conforms to the modern standards. + +The following four files demonstrate this approach: + +```html title="index.html - still grab the script so it appears as a global reference." + +... + + +... +``` + +```js title="wrapper.js - this grabs the JavaScript functionality from the global context and wraps it (exports it) in the modern standards compliant manner." +// get all utilities needed from the global. +const { escape, unescape } = globalThis.html; + +// export utilities like a standards compliant module would do. +export { escape, unescape }; +``` + +```toml title="pyscript.toml - configure your JS modules as before, but use your wrapper instead of the original module." +[js_modules.main] +# will simulate a standard JS module +"./wrapper.js" = "html_escaper" +``` + +```python title="main.py - just import the module as usual and make use of it." +from pyscript import document + +# import the module either via +from pyscript.js_modules import html_escaper +# or via +from pyscript.js_modules.html_escaper import escape, unescape + +# show on body: <> +document.body.append(html.escape("<>")) +``` + +You can see this approach in action here: + +[https://pyscript.com/@agiammarchi/floral-glade/v2](https://pyscript.com/@agiammarchi/floral-glade/v2) + +### A standard JavaScript module + +This is both the easiest and best way to import any standard JS module into +Python. + +You don't need to reference the script in your HTML, just define how the source +JavaScript module maps into the `pyscript.js_modules` namespace in your +configuration file, as explained above. + +That's it! + +Here is an example project that uses this approach: + +[https://pyscript.com/@agiammarchi/floral-glade/v3](https://pyscript.com/@agiammarchi/floral-glade/v3) + + +### My own JavaScript code + +If you have your own JavaScript work, just remember to write it as a standard +JavaScript module. Put simply, ensure you `export` the things you need to. For +instance, in the following fragment of JavaScript, the two functions are +exported from the module: + +```js title="code.js - containing two functions exported as capabilities of the module." +/* +Some simple JavaScript functions for example purposes. +*/ + +export function hello(name) { + return "Hello " + name; +} + +export function fibonacci(n) { + if (n == 1) return 0; + if (n == 2) return 1; + return fibonacci(n - 1) + fibonacci(n - 2); +} +``` + +Next, just reference this module in the usual way in your TOML or JSON +configuration file: + +```TOML title="pyscript.toml - references the code.js module so it will appear as the code module in the pyscript.js_modules namespace." +[js_modules.main] +"code.js" = "code" +``` + +In your HTML, reference your Python script with this configuration file: + +```html title="Reference the expected configuration file." + +``` + +Finally, just use your JavaScript module’s exported functions inside PyScript: + +```python title="Just call your bespoke JavaScript code from Python." +from pyscript.js_modules import code + + +# Just use the JS code from Python "as usual". +greeting = code.hello("Chris") +print(greeting) +result = code.fibonacci(12) +print(result) +``` + +You can see this in action in the following example project: + +[https://pyscript.com/@ntoll/howto-javascript/latest](https://pyscript.com/@ntoll/howto-javascript/latest) diff --git a/docs/user-guide/editor.md b/docs/user-guide/editor.md new file mode 100644 index 0000000..ba54f0e --- /dev/null +++ b/docs/user-guide/editor.md @@ -0,0 +1,248 @@ +# Python editor + +The PyEditor is a core plugin. + +!!! warning + + Work on the Python editor is in its early stages. We have made it available + in this version of PyScript to give the community an opportunity to play, + experiment and provide feedback. + + Future versions of PyScript will include a more refined, robust and perhaps + differently behaving version of the Python editor. + +If you specify the type of a ` + +``` + +However, different editors can share the same interpreter if they share the +same `env` attribute value. + +```html title="Two editors sharing the same MicroPython environment." + + +``` + +The outcome of these code fragments should look something like this: + + + +!!! info + + Notice that the interpreter type, and optional environment name is shown + at the top right above the Python editor. + + Hovering over the Python editor reveals the "run" button. + +### Stop evaluation + +Sometimes, for whatever reason, the fragment of code in the editor will never +complete. Perhaps it's stuck in an infinite loop and you need to stop the +evaluation of your code so you can fix the problem and start it again. + +When the code is running, hovering over the editor will reveal a stop button +(where the run button was found). Click on it, confirm you want to stop your +code, and then the code will stop and the editor will refresh so you can fix +your code. + +It looks something like this: + + + +### Setup + +Sometimes you need to create a pre-baked Pythonic context for a shared +environment used by an editor. This need is especially helpful in educational +situations where boilerplate code can be run, with just the important salient +code available in the editor. + +To achieve this end use the `setup` attribute within a `script` tag. The +content of this editor will not be shown, but will bootstrap the referenced +environment automatically before any following editor within the same +environment is evaluated. + +```html title="Bootstrapping an environment with `setup`" + + + +``` + +Finally, the `target` attribute allows you to specify a node into which the +editor will be rendered: + +```html title="Specify a target for the Python editor." + +

+``` + +## Editor VS Terminal + +The editor and terminal are commonly used to embed interactive Python code into +a website. However, there are differences between the two plugins, of which you +should be aware. + +The main difference is that a `py-editor` or `mpy-editor` is an isolated +environment (from the rest of PyScript that may be running on the page) and +its code always runs in a web worker. We do this to prevent accidental blocking +of the main thread that would freeze your browser's user interface. + +Because an editor is isolated from regular *py* or *mpy* scripts, one should +not expect the same behavior regular *PyScript* elements follow, most notably: + + * The editor's user interface is based on + [CodeMirror](https://codemirror.net/) and not on XTerm.js + [as it is for the terminal](../terminal). + * Code is evaluated all at once and asynchronously when the *Run* button is + pressed (not each line at a time, as in the terminal). + * The editor has listeners for `Ctrl-Enter` or `Cmd-Enter`, and + `Shift-Enter` to shortcut the execution of all the code. These shortcuts + make no sense in the terminal as each line is evaluated separately. + * There is a clear separation between the code and any resulting output. + * You may not use blocking calls (like `input`) with the editor, whereas + these will work if running the terminal via a worker. + * It's an editor! So simple or complex programs can be fully written without + running the code until ready. In the terminal, code is evaluated one line + at a time as it is typed in. + * There is no special reference to the underlying editor instance, while + there is both `script.terminal` or `__terminal__` in the terminal. + +## Read / Write / Execute + +Sometimes you need to programatically read, write or execute code in an +editor. Once PyScript has started, every py-editor/mpy-editor script tag gets +a `code` accessor attached to it. + +```python +from pyscript import document + +# Grab the editor script reference. +editor = document.querySelector('#editor') + +# Output the live content of the editor. +print(editor.code) + +# Update the live content of the editor. +editor.code = """ +a = 1 +b = 2 +print(a + b) +""" + +# Evaluate the live code in the editor. +# This could be any arbitrary code to evaluate in the editor's Python context. +editor.process(editor.code) +``` + +## Configuration + +Unlike ` + + +``` + +This +[live example](https://agiammarchi.pyscriptapps.com/pyeditor-iot-example/latest/) +shows how the editor can be used to execute code via a USB serial connection to +a connected MicroPython microcontroller. + +## Tab behavior + +We currently trap the `tab` key in a way that reflects what a regular code +editor would do: the code is simply indented, rather than focus moving to +another element. + +We are fully aware of the implications this might have around accessibility so +we followed +[this detailed advice from Codemirror's documentation](https://codemirror.net/examples/tab/) +We have an *escape hatch* to move focus outside the editor. Press `esc` before +`tab` to move focus to the next focusable element. Otherwise `tab` indents +code. + + +## Still missing + +The PyEditor is currently under active development and refinement, so features +may change (depending on user feedback). For instance, there is currently no +way to stop or kill a web worker that has got into difficulty from the editor +(hint: refreshing the page will reset things). diff --git a/docs/user-guide/features.md b/docs/user-guide/features.md new file mode 100644 index 0000000..cf0f2d3 --- /dev/null +++ b/docs/user-guide/features.md @@ -0,0 +1,87 @@ +# Features + +
+
All the web
+
+

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

+ +

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

+ +

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

+ +
All of Python
+
+

PyScript brings you two Python interpreters:

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

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

+

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

+

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

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

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

+ +

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

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

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

+ +

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

+ +

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

+
+
+ + diff --git a/docs/user-guide/ffi.md b/docs/user-guide/ffi.md index 60b0e58..a874599 100644 --- a/docs/user-guide/ffi.md +++ b/docs/user-guide/ffi.md @@ -1 +1,211 @@ -# Foreign Function Interface (FFI) +# PyScript FFI + +The foreign function interface (FFI) gives Python access to JavaScript, and +JavaScript access to Python. As a result PyScript is able to access all the +standard APIs and capabilities provided by the browser. + +We provide a unified `pyscript.ffi` because +[Pyodide's FFI](https://pyodide.org/en/stable/usage/api/python-api/ffi.html) +is only partially implemented in MicroPython and there are some fundamental +differences. The `pyscript.ffi` namespace smooths out such differences into +a uniform and consistent API. + +Our `pyscript.ffi` offers the following utilities: + +* `ffi.to_js(reference)` converts a Python object into its JavaScript + counterpart. +* `ffi.create_proxy(def_or_lambda)` proxies a generic Python function into a + JavaScript one, without destroying its reference right away. + +Should you require access to Pyodide or MicroPython's specific version of the +FFI you'll find them under the `pyodide.ffi` and `micropython.ffi` namespaces. +Please refer to the documentation for those projects for further information. + +## to_js + +In the +[Pyodide project](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.to_js), +this utility converts Python dictionaries into +[JavaScript `Map` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). +Such `Map` objects reflect the `obj.get(field)` semantics native to Python's +way of retrieving a value from a dictionary. + +Unfortunately, this default conversion breaks the vast majority of native and +third party JavaScript APIs. This is because the convention in idiomatic +JavaScript is to use an [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) +for such key/value data structures (not a `Map` instance). + +A common complaint has been the repeated need to call `to_js` with the long +winded argument `dict_converter=js.Object.fromEntries`. It turns out, most +people most of the time simply want to map a Python `dict` to a JavaScript +`object` (not a `Map`). + +Furthermore, in MicroPython the default Python `dict` conversion is to the +idiomatic and sensible JavaScript `object`, making the need to specify a +dictionary converter pointless. + +Therefore, if there is no reason to hold a Python reference in a JavaScript +context (which is 99% of the time, for common usage of PyScript) then use the +`pyscript.ffi.to_js` function, on both Pyodide and MicroPython, to always +convert a Python `dict` to a JavaScript `object`. + +```html title="to_js: pyodide.ffi VS pyscript.ffi" + + + + + +``` + +!!! Note + + It is still possible to specify a different `dict_converter` or use Pyodide + specific features while converting Python references by simply overriding + the explicit field for `dict_converter`. + + However, we cannot guarantee all fields and features provided by Pyodide + will work in the same way on MicroPython. + +## create_proxy + +In the +[Pyodide project](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#pyodide.ffi.create_proxy), +this function ensures that a Python callable associated with an event listener, +won't be garbage collected immediately after the function is assigned to the +event. Therefore, in Pyodide, if you do not wrap your Python function, it is +immediately garbage collected after being associated with an event listener. + +This is so common a gotcha (see the FAQ for +[more on this](../../faq#borrowed-proxy)) that the Pyodide project have already +created many work-arounds to address this situation. For example, the +`create_once_callable`, `pyodide.ffi.wrappers.add_event_listener` and +`pyodide.ffi.set_timeout` are all methods whose goal is to automatically manage +the lifetime of the passed in Python callback. + +Add to this situation methods connected to the JavaScript `Promise` object +(`.then` and `.catch` callbacks that are implicitly handled to guarantee no +leaks once executed) and things start to get confusing and overwhelming with +many ways to achieve a common end result. + +Ultimately, user feedback suggests folks simply want to do something like this, +as they write their Python code: + +```python title="Define a callback without create_proxy." +import js +from pyscript import window + + +def callback(msg): + """ + A Python callable that logs a message. + """ + window.console.log(msg) + + +# Use the callback without having to explicitly create_proxy. +js.setTimeout(callback, 1000, 'success') +``` + +Therefore, PyScript provides an experimental configuration flag called +`experimental_create_proxy = "auto"`. When set, you should never have to care +about these technical details nor use the `create_proxy` method and all the +JavaScript callback APIs should just work. + +Under the hood, the flag is strictly coupled with the JavaScript garbage +collector that will eventually destroy all proxy objects created via the +[FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) +built into the browser. + +This flag also won't affect MicroPython because it rarely needs a +`create_proxy` at all when Python functions are passed to JavaScript event +handlers. MicroPython automatically handles this situation. However, +there might still be rare and niche cases in MicroPython where such a +conversion might be needed. + +Hence, PyScript retains the `create_proxy` method, even though it does not +change much in the MicroPython world, although it might be still needed with +the Pyodide runtime is you don't use the `experimental_create_proxy = "auto"` +flag. + +At a more fundamental level, MicroPython doesn't provide (yet) a way to +explicitly destroy a proxy reference, whereas Pyodide still expects to +explicitly invoke `proxy.destroy()` when the function is not needed. + +!!! warning + + In MicroPython proxies might leak due to the lack of a `destroy()` method. + + Happily, proxies are usually created explicitly for event listeners or + other utilities that won't need to be destroyed in the future. So the lack + of a `destroy()` method in MicroPython is not a problem in this specific, + and most common, situation. + + Until we have a `destroy()` in MicroPython, we suggest testing the + `experimental_create_proxy` flag with Pyodide so both runtimes handle + possible leaks automatically. + +For completeness, the following examples illustrate the differences in +behaviour between Pyodide and MicroPython: + +```html title="A classic Pyodide gotcha VS MicroPython" + + + + + +``` + +To address the difference in Pyodide's behaviour, we can use the experimental +flag: + +```html title="experimental create_proxy" + + experimental_create_proxy = "auto" + + + + +``` + +Alternatively, `create_proxy` via the `pyscript.ffi` in both interpreters, but +only in Pyodide can we then destroy such proxy: + +```html title="pyscript.ffi.create_proxy" + + +``` diff --git a/docs/user-guide/filesystem.md b/docs/user-guide/filesystem.md new file mode 100644 index 0000000..b977a3f --- /dev/null +++ b/docs/user-guide/filesystem.md @@ -0,0 +1,176 @@ +# PyScript and Filesystems + +As you know, the filesystem is where you store files. For Python to work there +needs to be a filesystem in which Python packages, modules and data for your +apps can be found. When you `import` a library, or when you `open` a file, it +is on the in-browser virtual filesystem that Python looks. + +However, things are not as they may seem. + +This section clarifies what PyScript means by a filesystem, and the way in +which PyScript interacts with such a concept. + +## Two filesystems + +PyScript interacts with two filesystems. + +1. The browser, thanks to + [Emscripten](https://emscripten.org/docs/api_reference/Filesystem-API.html), + provides a virtual in-memory filesystem. **This has nothing to do with your + device's local filesystem**, but is contained within the browser based + sandbox used by PyScript. The [files](../configuration/#files) + configuration API defines what is found on this filesystem. +2. PyScript provides an easy to use API for accessing your device's local + filesystem. It requires permission from the user to mount a folder from the + local filesystem onto a directory in the browser's virtual filesystem. Think + of it as gate-keeping a bridge to the outside world of the device's local + filesystem. + +!!! danger + + Access to the device's local filesystem **is only available in Chromium + based browsers**. The maximum capacity for files shared in this way is + 4GB. + + Firefox and Safari do not support this capability (yet), and so it is not + available to PyScript running in these browsers. + +## The in-browser filesystem + +The filesystem that both Pyodide and MicroPython use by default is the +[in-browser virtual filesystem](https://emscripten.org/docs/api_reference/Filesystem-API.html). +Opening files and importing modules takes place in relation to this sandboxed +environment, configured via the [files](../configuration/#files) entry in your +settings. + +```toml title="Filesystem configuration via TOML." +[files] +"https://example.com/myfile.txt": "" +``` + +```python title="Just use the resulting file 'as usual'." +# Interacting with the virtual filesystem, "as usual". +with open("myfile.txt", "r") as myfile: + print(myfile.read()) +``` + +Currently, each time you re-load the page, the filesystem is recreated afresh, +so any data stored by PyScript to this filesystem will be lost. + +!!! info + + In the future, we may make it possible to configure the in-browser virtual + filesystem as persistent across re-loads. + +[This article](https://emscripten.org/docs/porting/files/file_systems_overview.html) +gives an excellent overview of the browser based virtual filesystem's +implementation and architecture. + +The most important key concepts to remember are: + +* The PyScript filesystem is contained *within* the browser's sandbox. +* Each instance of a Python interpreter used by PyScript runs in a separate + sandbox, and so does NOT share virtual filesystems. +* All Python related filesytem operations work as expected with this + filesystem. +* The virtual filesystem is configured via the + [files](../configuration/#files) entry in your settings. +* The virtual filesystem is (currently) NOT persistent between page re-loads. +* Currently, the filesystem has a maximum capacity of 4GB of data (something + over which we have no control). + +## The device's local filesystem + +**Access to the device's local filesystem currently only works on Chromium +based browsers**. + +Your device (the laptop, mobile or tablet) that runs your browser has a +filesystem provided by a hard drive. Thanks to the +[`pyscript.fs` namespace in our API](../../api/#pyscriptfs), both MicroPython +and Pyodide (CPython) gain access to this filesystem should the user of +your code allow this to happen. + +This is a [transient activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation) +for the purposes of +[user activation of gated features](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation). +Put simply, before your code gains access to their local filesystem, an +explicit agreement needs to be gathered from the user. Part of this process +involves asking the user to select a target directory on their local +filesystem, to which PyScript will be given access. + +The directory on their local filesystem, selected by the user, is then mounted +to a given directory inside the browser's virtual filesystem. In this way a +mapping is made between the sandboxed world of the browser, and the outside +world of the user's filesystem. + +Your code will then be able to perform all the usual filesystem related +operations provided by Python, within the mounted directory. However, **such +changes will NOT take effect on the local filesystem UNTIL your code +explicitly calls the `sync` function**. At this point, the state of the +in-browser virtual filesystem and the user's local filesystem are synchronised. + +The following code demonstrates the simplest use case: + +```python title="The core operations of the pyscript.fs API" +from pyscript import fs + +# Ask once for permission to mount any local folder +# into the virtual filesystem handled by Pyodide/MicroPython. +# The folder "/local" refers to the directory on the virtual +# filesystem to which the user-selected directory will be +# mounted. +await fs.mount("/local") + +# ... DO FILE RELATED OPERATIONS HERE ... + +# If changes were made, ensure these are persisted to the local filesystem's +# folder. +await fs.sync("/local") + +# If needed to free RAM or that specific path, sync and unmount +await fs.unmount("/local") +``` + +It is possible to use multiple different local directories with the same mount +point. This is important if your application provides some generic +functionality on data that might be in different local directories because +while the nature of the data might be similar, the subject is not. For +instance, you may have different models for a PyScript based LLM in different +directories, and may wish to switch between them at runtime using different +handlers (requiring their own transient action). In which case use +the following technique: + +```python title="Multiple local directories on the same mount point" +# Mount a local folder specifying a different handler. +# This requires a user explicit transient action (once). +await fs.mount("/local", id="v1") +# ... operate on that folder ... +await fs.unmount("/local") + +# Mount a local folder specifying a different handler. +# This also requires a user explicit transient action (once). +await fs.mount("/local", id="v2") +# ... operate on that folder ... +await fs.unmount("/local") + +# Go back to the original handler or a previous one. +# No transient action required now. +await fs.mount("/local", id="v1") +# ... operate again on that folder ... +``` + +In addition to the mount `path` and handler `id`, the `fs.mount` function can +take two further arguments: + +* `mode` (by default `"readwrite"`) indicates the sort of activity available to + the user. It can also be set to `read` for read-only access to the local + filesystem. This is a part of the + [web-standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#mode) + for directory selection. +* `root` - (by default, `""`) is a hint to the browser for where to start + picking the path that should be mounted in Python. Valid values are: + `desktop`, `documents`, `downloads`, `music`, `pictures` or `videos` + [as per web standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#startin). + +The `sync` and `unmount` functions only accept the mount `path` used in the +browser's local filesystem. diff --git a/docs/user-guide/first-steps.md b/docs/user-guide/first-steps.md new file mode 100644 index 0000000..6a01580 --- /dev/null +++ b/docs/user-guide/first-steps.md @@ -0,0 +1,165 @@ +# First steps + +It's simple: + +* tell your browser to use PyScript, then, +* tell PyScript how to run your Python code. + +That's it! + +For the browser to use PyScript, simply add a ` + + + + + +``` + +There are two ways to tell PyScript how to find your code. + +* With a standard HTML ` +``` + +...and here's a `` tag with inline Python code. + +```html title="A <py-script> tag with inline code" + +import sys +from pyscript import display + + +display(sys.version) + +``` + +The ` + + ``` + + Notice how different interpreters can be used with different + configurations. + + diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index b35b5a0..0351907 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -1,485 +1,41 @@ -# Introduction (start here) +# The PyScript User Guide -!!! note - - This guide provides detailed technical guidance for developers who want - an in depth explanation of the PyScript platform. - - As a result, while we endeavour to write clearly, some of the content in - this user guide will not be suitable for beginners. We assume the folks who - will get most from this guide will already have some Python or web - development experience. - - We welcome feedback to help us improve. - -This guide is in three parts: - -1. The brief overview, context setting and sign-posting contained within this - page. -2. The other pages of the guide, referenced from this page, that explore the - different aspects of PyScript in substantial technical detail. -3. The example projects, listed at the end of this page, that demonstrate the - various features of PyScript working together in real-world applications. - -We suggest you _read this page in full_: it will ensure you have a -comprehensive overview of the PyScript platform along with suggestions for -where next to explore. - -When you require depth and technical detail you should consult the other pages -of this guide that are referenced from this page. They provide clear and -precise details along with example code fragments and descriptions of the APIs -available via PyScript. - -Finally, the examples listed at the end of this page are all freely available -and copiously commented on [pyscript.com](pyscript.com). You should consult -these for practical "real world" use of the various features of the PyScript -platform. Many of these examples come from contributors to our wonderful -community. If you believe you have a project that would make a good example, -please don't hesitate to get in touch. - -## What is PyScript? - -PyScript is a platform for Python in the browser. - -PyScript's aim is to bring together two of the most vibrant technical -ecosystems on the planet. If the web and Python had a baby, you'd get PyScript. - -PyScript works because modern browsers support -[web assembly](https://webassembly.org/) (abbreviated to WASM) - a virtual -machine with an open specification and near native performance. PyScript takes -versions of the Python interpreter compiled to WASM, and makes them easy to use -from within your browser. - -At the core of PyScript is a philosophy of digital empowerment. The web is the -world's most ubiquitous computing platform, mature and familiar to billions of -people. Python is one of the world's most popular programming languages: -easy to teach and learn, used in a plethora of existing domains -such as data science, games, embedded systems, artificial intelligence and -film making (to name but a few), and the Python ecosystem contains a huge -number of libraries to address its many uses. - -PyScript brings together the ubiquity and accessibility of the web with the -power, depth and expressiveness of Python. It means PyScript isn't just for -programming experts but, as we like to say, for the 99% of the rest of the -planet who use computers. - -## Features - -
-
All the web
-
-

Thanks to the foreign function interface (FFI), PyScript gives you - access to all the - web - APIs implemented by your browser.

- -

The FFI makes it easy for Python to work within your browser, including - with third party JavaScript libraries that may be included via the - script tag.

- -

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

-
All of Python
-
-

PyScript brings you two Python interpreters:

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

Because it is just regular CPython, Pyodide puts Python's deep and - diverse ecosystem of libraries, frameworks and modules at your disposal. If - you find yourself encountering some sort of computing problem, there's - probably a Python library to help you with it. If you're used to using a - favourite library in Python, now you can use it in your browser and share - it with the ease of a URL.

-

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

-

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

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

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

- -

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

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

As you'll see, PyScript has a small, efficient yet powerful core called - PolyScript.

-

Most of the functionality of PyScript is actually implemented through - PolyScript's plugin system.

- -

This approach means we get a clear separation of concerns: PolyScript - can focus on being small, efficient and powerful, whereas the PyScript - related plugins allow us to build upon the generic features provided by - PolyScript. More importantly, because there is a plugin system, folks - _independent of the PyScript core team_ have a way to create their own - plugins so we get a rich ecosystem of functionality that reflects the - unique and many needs of PyScript's users.

-
-
- -## Architecture - -There are two important pieces of information you should know about the -architecture of PyScript: - -1. A small, efficient and powerful kernel called - [PolyScript](https://github.com/pyscript/polyscript) is the foundation - upon which PyScript and plugins are built. -2. The stack inside the browser is relatively simple and easy to understand. - -Let's dive into each in turn. - -### PolyScript - -[PolyScript](https://github.com/pyscript/polyscript) is the core of PyScript. - -Its purpose is to bootstrap the platform and provide all the necessary core -capabilities. It is a small, efficient and powerful kernel. Setting aside -PyScript for a moment, to use *just PolyScript* requires a ` - - - - - - -``` - -PolyScript provides a -[small yet powerful set of capabilities](https://pyscript.github.io/polyscript/) -upon which PyScript itself is built. - -These can be summarised as: - -* Evaluation of code via ` - - - - - -``` - -There are two ways to tell PyScript where to find your code. - -* With a standard HTML ` -``` +Our docs have three aims: -...and here's the `` tag with inline Python code. +1. A [clear overview](what.md) of all things PyScript. +2. [Exploration of PyScript](architecture.md) in substantial technical detail. +3. Demonstration of the features of PyScript working together in + [real-world example applications](../examples.md). -```html title="Using the py-script tag with inline code" - -import sys +_Read this user guide in full_: it is a short but comprehensive overview of the +PyScript platform. +Get involved! Join in the PyScript conversation on our +[discord server](https://discord.gg/HxvBtukrg2). There you'll find core +developers, community contributors and a flourishing forum for those creating +projects with PyScript. Should you wish to engage with the development of +PyScript, you are welcome to contribute via +[the project's GitHub organisation](https://github.com/pyscript). -print(sys.version) - -``` +Finally, the example projects referenced in our docs are all freely available +and copiously commented on [pyscript.com](https://pyscript.com). -Both tags accept various attributes to control their behaviour. More detailed -information can be found on the [page about code](code). - -### Interpreters - -Python is an interpreted language, and thus needs an interpreter to work. - -PyScript currently supports two versions of the Python interpreter that have -been compiled to WASM: Pyodide and MicroPython. You should select which one to -use depending on your use case and acceptable trade-offs. - -Both interpreters make use of [emscripten](https://emscripten.org/), a compiler -toolchain (using LLVM), for emitting WASM assets for the browser. Emscripten -also provides APIs so operating-system level features such as a sandboxed file -system (**not** the user's local machine's filesystem), IO (`stdin`, `stdout`, -`stderr` etc,) and networking are available within the context of a browser. - -#### Pyodide - -[Pyodide](https://pyodide.org/) is a version of the standard -[CPython](https://python.org/) interpreter, patched to compile to WASM and -work in the browser. - -It is a mature and stable build of the CPython interpreter and includes many -useful features: - -* A robust [Python](https://pyodide.org/en/stable/usage/api/python-api.html) - ⟺ [JavaScript](https://pyodide.org/en/stable/usage/api/js-api.html) foreign - function interface (FFI). This bridges the gap between the browser and Python - worlds. -* The installation of pure Python packages from [PyPI](https://pypi.org/) via - the [micropip](https://micropip.pyodide.org/en/stable/index.html) package - installer. Some packages with C extensions have versions compiled for WASM - and these can also be installed with `micropip`. There are plans afoot to - make WASM a target in PyPI so packages with C extenions can be automatically - compiled to WASM. -* An active, friendly and technically outstanding team of volunteer - contributors (some of whom have been supported by the PyScript project). -* Extensive official - [documentation](https://micropip.pyodide.org/en/stable/index.html), and many - tutorials found online. -* Builds of Pyodide that include popular packages for data science like - [Numpy](https://numpy.org/), [Scipy](https://scipy.org/) and - [Pandas](https://pandas.pydata.org/). - -#### MicroPython - -[MicroPython](https://micropython.org/) is a lean and efficient implementation -of the Python 3 programming language that includes a small subset of the Python -standard library and is optimised to run on microcontrollers and in constrained -environments (like the browser). - -Everything needed to view a web page in a browser needs to be delivered -over the network. The smaller the asset to be delivered can be, the better. -MicroPython, when compressed for delivery to the browser, is only around -170k in size - smaller than most images you find on most websites. - -This makes MicroPython particularly suited to browsers running in a more -constrained environment such as on a mobile or tablet based device. Browsing -with these devices usually uses (slower) mobile internet connections. -Furthermore, because MicroPython is lean and efficient it still performs -exceptionally well on these relatively underpowered devices. - -Thanks to collaboration between the MicroPython and PyScript projects, there is -a foreign function interface for MicroPython. The MicroPython FFI deliberately -copies the API of the FFI originally written for Pyodide - meaning it is -relatively easy to migrate between the two supported interpreters. - -Further details and more in-depth discussion of the interpreters supported by -PyScript can be found on [the interpreters page](interpreters). - -### The DOM - -The DOM -([document object model](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model)) -refers to a tree like data structure that represents the web page in the -browser. PyScript needs to be able to interact with the DOM in order to change -the user interface and react to things happening in the browser. - -There are currently two ways to interact with the DOM: - -1. Through the FFI and by directly interacting with the objects found in the - `globalThis` object. -2. Through the `pydom` module that comes as standard with PyScript. - -The first option gives you access to all the [standard web capabilities and -features](https://developer.mozilla.org/en-US/docs/Web), such as the browser's -built-in [web APIs](https://developer.mozilla.org/en-US/docs/Web/API), and the -`document` object at the root of the [DOM tree](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Using_the_Document_Object_Model#what_is_a_dom_tree). - -The second is a Python module called `pydom` that wraps many (although not all) -the features available via the FFI in a more idiomatically Pythonic library... -**TODO: Fabio to finish this bit...** - -Explore the PyScript story of working with the DOM in [the DOM page](dom). - -### Configuration - -Sometimes we need to tell PyScript about how we want our Python environment to -be configured. To this end there are three core options: - -* `fetch` files from URLs onto the filesystem emulated by the browser for your - web page. -* A list of Python `packages` to be installed from [PyPI](https://pypi.org/) - onto the filesystem by Pyodide's - [micropip](https://micropip.pyodide.org/en/stable/index.html) package - installer. -* A list of `plugins` to be enabled by PyScript to add extra functionality to - the platform. - -!!! warning - - Because `micropip` is a Pyodide-only feature, and MicroPython doesn't - support code packaged on PyPI, **the `packages` option is only available - if you use Pyodide as your interpreter**. - -#### TOML or JSON - -Configuration can be expressed in two formats: - -* [TOML](https://toml.io/en/) is the configuration file format most often used - by folks in the Python community. -* [JSON](https://www.json.org/json-en.html) is a data format most often used - by folks in the web community. - -Since PyScript is the marriage of Python and the web, we support both. - -However, because JSON is built into all browsers by default and TOML requires -an additional download of a specialist parser before PyScript can work, the -use of JSON is more efficient from a performance point of view. - -The following two configurations are equivalent, and simply tell PyScript to -ensure the packages `arrr` and `numberwang` are installed from PyPI - -```TOML title="Configuration via TOML" -packages = ["arrr", "numberwang" ] -``` - -```JSON title="Configuration via JSON" -{ - "packages": ["arrr", "numberwang"] -} -``` - -#### File based or inline configuration - -The recommended way to write configurations is via a separate file and -referencing it from the tag used to specify the Python code: - -```HTML title="Reference a configuration file" - -``` - -For historical and convenience reasons we still support the inline -specification of configuration information via the `` tag used -in your HTML document: - -```HTML title="Inline configuration via the <py-config> tag" - -{ - "packages": ["arrr", "numberwang" ] -} - -``` - -Fully worked out examples of how to configure each of the `fetch`, `packages` -and `plugins` options, along with details of how to define arbitrary -additional configuration options (that plugins may require) can be found on -[the configuration page](configuration). - -### Workers - -### Plugins - -### FFI - -## Examples - -### Lots of DOM manipulation - -### Data science-y - -### Graphical - -### Blocking with workers +!!! note -### Calling an API + Many of these examples come from contributors in our wonderful + community. We love to recognise, share and celebrate the incredible work + of folks in the PyScript community. If you believe you have a project that + would make a good demonstration, please don't hesitate to + [get in touch](https://discord.gg/HxvBtukrg2). diff --git a/docs/user-guide/interpreters.md b/docs/user-guide/interpreters.md deleted file mode 100644 index bbc42d0..0000000 --- a/docs/user-guide/interpreters.md +++ /dev/null @@ -1 +0,0 @@ -# Python Interpreters diff --git a/docs/user-guide/media.md b/docs/user-guide/media.md new file mode 100644 index 0000000..91e45a7 --- /dev/null +++ b/docs/user-guide/media.md @@ -0,0 +1,372 @@ +# PyScript and Media Devices + +For web applications to interact with cameras, microphones, and other media +devices, there needs to be a way to access these hardware components through the +browser. PyScript provides a media API that enables your Python code to interact +with media devices directly from the browser environment. + +This section explains how PyScript interacts with media devices and how you can +use these capabilities in your applications. + +## Media Device Access + +PyScript interacts with media devices through the browser's [MediaDevices +API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices). This API +provides access to connected media input devices like cameras and microphones, +as well as output devices like speakers. + +When using PyScript's media API, it's important to understand: + +1. Media access requires **explicit user permission**. The browser will show a + permission dialog when your code attempts to access cameras or microphones. +2. Media access is only available in **secure contexts** (HTTPS or localhost). +3. All media interactions happen within the **browser's sandbox**, following the + browser's security policies. + +## The `pyscript.media` API + +PyScript provides a Pythonic interface to media devices through the +`pyscript.media` namespace. This API includes two main components: + +1. The `Device` class - represents a media device and provides methods to + interact with it +2. The `list_devices()` function - discovers available media devices + +### Listing Available Devices + +To discover what media devices are available, use the `list_devices()` function: + +```python +from pyscript.media import list_devices + +async def show_available_devices(): + devices = await list_devices() + for device in devices: + print(f"Device: {device.label}, Type: {device.kind}, ID: {device.id}") + +# List all available devices +show_available_devices() +``` + +This function returns a list of `Device` objects, each representing a media +input or output device. Note that the browser will typically request permission +before providing this information. + +### Working with the Camera + +The most common use case is accessing the camera to display a video stream: + +```python +from pyscript import when +from pyscript.media import Device +from pyscript.web import page + +async def start_camera(): + # Get a video stream (defaults to video only, no audio) + stream = await Device.load(video=True) + + # Connect the stream to a video element in your HTML + video_element = page["#camera"][0]._dom_element + video_element.srcObject = stream + + return stream + +# Start the camera +camera_stream = start_camera() +``` + +The `Device.load()` method is a convenient way to access media devices without +first listing all available devices. You can specify options to control which +camera is used: + +```python +# Prefer the environment-facing camera (often the back camera on mobile) +stream = await Device.load(video={"facingMode": "environment"}) + +# Prefer the user-facing camera (often the front camera on mobile) +stream = await Device.load(video={"facingMode": "user"}) + +# Request specific resolution +stream = await Device.load(video={ + "width": {"ideal": 1280}, + "height": {"ideal": 720} +}) +``` + +### Capturing Images from the Camera + +To capture a still image from the video stream: + +```python +def capture_image(video_element): + # Get the video dimensions + width = video_element.videoWidth + height = video_element.videoHeight + + # Create a canvas to capture the frame + canvas = document.createElement("canvas") + canvas.width = width + canvas.height = height + + # Draw the current video frame to the canvas + ctx = canvas.getContext("2d") + ctx.drawImage(video_element, 0, 0, width, height) + + # Get the image as a data URL + image_data = canvas.toDataURL("image/png") + + return image_data +``` + +For applications that need to process images with libraries like OpenCV, you +need to convert the image data to a format these libraries can work with: + +```python +import numpy as np +import cv2 + +def process_frame_with_opencv(video_element): + # Get video dimensions + width = video_element.videoWidth + height = video_element.videoHeight + + # Create a canvas and capture the frame + canvas = document.createElement("canvas") + canvas.width = width + canvas.height = height + ctx = canvas.getContext("2d") + ctx.drawImage(video_element, 0, 0, width, height) + + # Get the raw pixel data + image_data = ctx.getImageData(0, 0, width, height).data + + # Convert to numpy array for OpenCV + frame = np.asarray(image_data, dtype=np.uint8).reshape((height, width, 4)) + + # Convert from RGBA to BGR (OpenCV's default format) + frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) + + # Process the image with OpenCV + # ... + + return frame_bgr +``` + +### Managing Camera Resources + +It's important to properly manage media resources, especially when your +application no longer needs them. Cameras and microphones are shared resources, +and failing to release them can impact other applications or cause unexpected +behavior. + +### Stopping the Camera + +To stop the camera and release resources: + +```python +from pyscript.web import page + +def stop_camera(stream): + # Stop all tracks on the stream + if stream: + tracks = stream.getTracks() + for track in tracks: + track.stop() + + # Clear the video element's source + video_element = page["#camera"][0]._dom_element + if video_element: + video_element.srcObject = None +``` + +### Switching Between Cameras + +For devices with multiple cameras, you can implement camera switching: + +```python +from pyscript.media import Device, list_devices +from pyscript.web import page + +class CameraManager: + def __init__(self): + self.cameras = [] + self.current_index = 0 + self.active_stream = None + self.video_element = page["#camera"][0]._dom_element + + async def initialize(self): + # Get all video input devices + devices = await list_devices() + self.cameras = [d for d in devices if d.kind == "videoinput"] + + # Start with the first camera + if self.cameras: + await self.start_camera(self.cameras[0].id) + + async def start_camera(self, device_id=None): + # Stop any existing stream + await self.stop_camera() + + # Start a new stream + video_options = ( + {"deviceId": {"exact": device_id}} if device_id + else {"facingMode": "environment"} + ) + self.active_stream = await Device.load(video=video_options) + + # Connect to the video element + if self.video_element: + self.video_element.srcObject = self.active_stream + + async def stop_camera(self): + if self.active_stream: + tracks = self.active_stream.getTracks() + for track in tracks: + track.stop() + self.active_stream = None + + if self.video_element: + self.video_element.srcObject = None + + async def switch_camera(self): + if len(self.cameras) <= 1: + return + + # Move to the next camera + self.current_index = (self.current_index + 1) % len(self.cameras) + await self.start_camera(self.cameras[self.current_index].id) +``` + +## Working with Audio + +In addition to video, the PyScript media API can access audio inputs: + +```python +# Get access to the microphone (audio only) +audio_stream = await Device.load(audio=True, video=False) + +# Get both audio and video +av_stream = await Device.load(audio=True, video=True) +``` + +## Best Practices + +When working with media devices in PyScript, follow these best practices: + +### Permissions and User Experience + +1. **Request permissions contextually**: + - Only request camera/microphone access when needed + - Explain to users why you need access before requesting it + - Provide fallback options when permissions are denied + +2. **Clear user feedback**: + - Indicate when the camera is active + - Provide controls to pause/stop the camera + - Show loading states while the camera is initializing + +### Resource Management + +1. **Always clean up resources**: + - Stop media tracks when they're not needed + - Clear `srcObject` references from video elements + - Be especially careful in single-page applications + +2. **Handle errors gracefully**: + - Catch exceptions when requesting media access + - Provide meaningful error messages + - Offer alternatives when media access fails + +### Performance Optimization + +1. **Match resolution to needs**: + - Use lower resolutions when possible + - Consider mobile device limitations + - Adjust video constraints based on the device + +2. **Optimize image processing**: + - Process frames on demand rather than continuously + - Use efficient algorithms + - Consider downsampling for faster processing + +## Example Application: Simple Camera Capture + +Here's a simplified example that shows how to capture and display images from a +camera: + +```python +from pyscript import when, window +from pyscript.media import Device +from pyscript.web import page + +class CameraCapture: + def __init__(self): + # Get UI elements + self.video = page["#camera"][0] + self.video_element = self.video._dom_element + self.capture_button = page["#capture-button"] + self.snapshot = page["#snapshot"][0] + + # Start camera + self.initialize_camera() + + async def initialize_camera(self): + # Prefer environment-facing camera on mobile devices + stream = await Device.load(video={"facingMode": "environment"}) + self.video_element.srcObject = stream + + def take_snapshot(self): + """Capture a frame from the camera and display it""" + # Get video dimensions + width = self.video_element.videoWidth + height = self.video_element.videoHeight + + # Create canvas and capture frame + canvas = window.document.createElement("canvas") + canvas.width = width + canvas.height = height + + # Draw the current video frame to the canvas + ctx = canvas.getContext("2d") + ctx.drawImage(self.video_element, 0, 0, width, height) + + # Convert the canvas to a data URL and display it + image_data_url = canvas.toDataURL("image/png") + self.snapshot.setAttribute("src", image_data_url) + +# HTML structure needed: +# +# +# + +# Usage: +# camera = CameraCapture() +# +# @when("click", "#capture-button") +# def handle_capture(event): +# camera.take_snapshot() +``` + +This example demonstrates: +- Initializing a camera with the PyScript media API +- Accessing the camera stream and displaying it in a video element +- Capturing a still image from the video stream when requested +- Converting the captured frame to an image that can be displayed + +This simple pattern can serve as the foundation for various camera-based +applications and can be extended with image processing libraries as needed for +more complex use cases. + + +## Conclusion + +The PyScript media API provides a powerful way to access and interact with +cameras and microphones directly from Python code running in the browser. By +following the patterns and practices outlined in this guide, you can build +sophisticated media applications while maintaining good performance and user +experience. + +Remember that media access is a sensitive permission that requires user consent +and should be used responsibly. Always provide clear indications when media +devices are active and ensure proper cleanup of resources when they're no longer +needed. diff --git a/docs/user-guide/offline.md b/docs/user-guide/offline.md new file mode 100644 index 0000000..919f387 --- /dev/null +++ b/docs/user-guide/offline.md @@ -0,0 +1,283 @@ +# Use PyScript Offline + +Sometimes you want to run PyScript applications offline. + +Both PyScript core and the interpreter used to run code need to be served with +the application itself. The two requirements needed to create an offline +version of PyScript are: + +1. Download and include PyScript core. +2. Download and include the Python interpreters used in your application. + +## Get PyScript core + +You have two choices: + + 1. **Build from source**. Clone the repository, install dependencies, then + build and use the content found in the `./dist/` folder. + 2. **Grab the npm package**. For simplicity this is the method we currently + recommend as the easiest to get started. + +In the following instructions, we assume the existence of a folder called +`pyscript-offline`. All the necessary files needed to use PyScript offline will +eventually find their way in there. + +In your computer's command line shell, create the `pyscript-offline` folder +like this: + +```sh +mkdir -p pyscript-offline +``` + +Now change into the newly created directory: + +```sh +cd pyscript-offline +``` + +### PyScipt core from source + +Build PyScript core by cloning the project repository and follow the +instructions in our [developer guide](../developers.md) + +Once completed, copy the `dist` folder, that has been created by the build +step, into your `pyscript-offline` folder. + +### PyScript core from `npm` + +Ensure you are in the `pyscript-offline` folder created earlier. + +Create a `package.json` file. Even an empty one with just `{}` as content will +suffice. This is needed to make sure our folder will include the local +`npm_modules` folder instead of placing assets elsewhere. Our aim is to ensure +everything is in the same place locally. + +```sh +# only if there is no package.json, create one +echo '{}' > ./package.json +``` + +Assuming you have +[npm installed on your computer](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), +issue the following command in the `pyscript-offline` folder to install the +PyScript core package. + +``` +# install @pyscript/core +npm i @pyscript/core +``` + +Now the folder should contain a `node_module` folder in it, and we can copy the +`dist` folder found within the `@pyscript/core` package wherever we like. + +```sh +# create a public folder to serve locally +mkdir -p public + +# move @pyscript/core dist into such folder +cp -R ./node_modules/@pyscript/core/dist ./public/pyscript +``` + +That's almost it! + +## Set up your application + +Simply create a `./public/index.html` file that loads the local PyScript: + +```html + + + + + + Codestin Search App + + + + + + + +``` + +Run this project directly (after being sure that `index.html` file is saved +into the `public` folder): + +```sh +python3 -m http.server -d ./public/ +``` + +If you would like to test `worker` features, try instead: + +```sh +npx mini-coi ./public/ +``` + +## Download a local interpreter + +PyScript officially supports *MicroPython* and *Pyodide* interpreters, so let's +see how to get a local copy for each one of them. + +### Local MicroPython + +Similar to `@pyscript/core`, we can also install *MicroPython* from *npm*: + +```sh +npm i @micropython/micropython-webassembly-pyscript +``` + +Our `node_modules` folder should contain a `@micropython` one and from there we +can move relevant files into our `public` folder. + +Let's be sure we have a target for that: + +```sh +# create a folder in our public space +mkdir -p ./public/micropython + +# copy related files into such folder +cp ./node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./public/micropython/ +``` + +The folder should contain at least both `micropython.mjs` and +`micropython.wasm` files. These are the files to use locally via a dedicated +config. + +```html + + + + + + Codestin Search App + + + + + + interpreter = "/micropython/micropython.mjs" + + + + +``` + +### Local Pyodide + +Remember, Pyodide uses `micropip` to install third party packages. While the +procedure for offline Pyodide is very similar to the one for MicroPython, +if we want to use 3rd party packages we also need to have these available +locally. We'll start simple and cover such packaging issues at the end. + +```sh +# locally install the pyodide module +npm i pyodide + +# create a folder in our public space +mkdir -p ./public/pyodide + +# move all necessary files into that folder +cp ./node_modules/pyodide/pyodide* ./public/pyodide/ +cp ./node_modules/pyodide/python_stdlib.zip ./public/pyodide/ +``` + +Please **note** that the `pyodide-lock.json` file is needed, so please don't +change that `cp` operation as all `pyodide*` files need to be moved. + +At this point, all we need to do is to change the configuration on our *HTML* +page to use *pyodide* instead: + +```html + + + + + + Codestin Search App + + + + + + interpreter = "/pyodide/pyodide.mjs" + + + + +``` + +## Wrap up + +That's basically it! + +Disconnect from the internet, run the local server, and the page will still +show that very same `Hello from PyScript` message. + +## Local Pyodide packages + +Finally, we need the ability to install Python packages from a local source +when using Pyodide. + +Put simply, we use the packages bundle from +[pyodide releases](https://github.com/pyodide/pyodide/releases/tag/0.26.2). + +!!! warning + + This bundle is more than 200MB! + + It contains each package that is required by Pyodide, and Pyodide will only + load packages when needed. + +Once downloaded and extracted (we're using version `0.26.2` in this example), +we can simply copy the files and folders inside the `pyodide-0.26.2/pyodide/*` +directory into our `./public/pyodide/*` folder. + +Feel free to either skip or replace the content, or even directly move the +`pyodide` folder inside our `./public/` one. + +Now use any package available in via the Pyodide bundle. + +For example: + +```html + + + + + + Codestin Search App + + + + + + interpreter = "/pyodide/pyodide.mjs" + packages = ["pandas"] + + + + +``` + +We should now be able to read `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` on the +page *even* if we disconnect from the Internet. + +That's it! diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md index dd95e28..d0d2825 100644 --- a/docs/user-guide/plugins.md +++ b/docs/user-guide/plugins.md @@ -1,2 +1,209 @@ -# PyScript Plugins +# Plugins +PyScript offers a plugin API _so anyone can extend its functionality and +share their modifications_. + +PyScript only supports plugins written in Javascript (although causing the +evaluation of bespoke Python code can be a part of such plugins). The plugin's +JavaScript code should be included on the web page via a +` + + + + + + + + +``` diff --git a/docs/user-guide/pygame-ce.md b/docs/user-guide/pygame-ce.md new file mode 100644 index 0000000..4cdf4a3 --- /dev/null +++ b/docs/user-guide/pygame-ce.md @@ -0,0 +1,321 @@ +# PyGame Support + +!!! Danger + + **Support for PyGame-CE is experimental** and its behaviour is likely to + change as we get feedback and bug reports from the community. + + Please bear this in mind as you try PyGame-CE with PyScript, and all + feedback, bug reports and constructive critique is welcome via discord + or GitHub. + + +[PyGameCE](https://pyga.me/) is a Python library for building powerful games +(so says their website). They also say, to get started you just need to +`pip install pygame-ce`. + +Thanks to work in the upstream [Pyodide project](https://pyodide.org/) +PyGame-CE is available in PyScript and to get started all you need to do is: +`` Now you don't even need to +`pip install` the library! It comes with PyScript by default, and you can share +your games via a URL! + +!!! Info + + Please refer to + [PyGame-CE's extensive documentation](https://pyga.me/docs/) for how to + create a game. Some things may not work because we're running in a + browser context, but play around and let us know how you get on. + +## Getting Started + +Here are some notes on using PyGame-CE specifically in a browser context with +pyscript versus running locally per +[PyGame-CE's documentation](https://pyga.me/docs/). + +1. You can use [pyscript.com](https://pyscript.com) as mentioned in + [Beginning PyScript](../beginning-pyscript.md) for an easy starting + environment. +2. Pyscript's PyGame-CE is under development, so make sure to use the latest + version by checking the `index.html` and latest version on this website. If + using [pyscript.com](https://pyscript.com), the latest version is not always + used in a new project. +3. The game loop needs to allow the browser to run to update the canvas used as + the game's screen. In the simplest projects, the quickest way to do that is + to replace `clock.tick(fps)` with `await asyncio.sleep(1/fps)`, but there + are better ways (discussed later). +4. If you have multiple Python source files or media such as images or sounds, + you need to use the [config attribute](configuration.md) to load the + files into the PyScript environment. The below example shows how to do this. +5. The integrated version of Python and PyGame-CE may not be the latest. In the + browser's console when PyGame-CE starts you can see the versions, and for + example if 2.4.1 is included, you can't use a function marked in the + documentation as "since 2.5". + +### Example + +This is the example quickstart taken from the [Python Pygame +Introduction](https://pyga.me/docs/tutorials/en/intro-to-pygame.html) on the +PyGame-CE website, modified only to add `await asyncio.sleep(1/60)` (and the +required `import asyncio`) to limit the game to roughly 60 fps. + +Note: since the `await` is not in an `async` function, it cannot run using +Python on your local machine, but a solution is +[discussed later](#running-locally). + + +```python title="quickstart.py" +import asyncio +import sys, pygame + +pygame.init() + +size = width, height = 320, 240 +speed = [2, 2] +black = 0, 0, 0 + +screen = pygame.display.set_mode(size) +ball = pygame.image.load("intro_ball.gif") +ballrect = ball.get_rect() + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: sys.exit() + + ballrect = ballrect.move(speed) + if ballrect.left < 0 or ballrect.right > width: + speed[0] = -speed[0] + if ballrect.top < 0 or ballrect.bottom > height: + speed[1] = -speed[1] + + screen.fill(black) + screen.blit(ball, ballrect) + pygame.display.flip() + await asyncio.sleep(1/60) +``` + +To run this game with PyScript, use the following HTML file, ensuring a call +to the Python program and a `` element where the graphics +will be placed. Make sure to update the pyscript release to the latest version. + +```html title="index.html" + + + + + Codestin Search App + + + + + + + + + + +``` + +!!! Info + + The `style="image-rendering: pixelated` on the canvas preserves the + pixelated look on high-DPI screens or when zoomed-in. Remove it to have a + "smoothed" look. + +Lastly, you need to define the `pyscript.toml` file to expose any files that +your game loads -- in this case, `intro_ball.gif` +[(download from pygame GitHub)](https://github.com/pygame-community/pygame-ce/blob/80fe4cb9f89aef96f586f68d269687572e7843f6/docs/reST/tutorials/assets/intro_ball.gif?raw=true). + +```toml title="pyscript.toml" +[files] +"intro_ball.gif" = "" +``` + +Now you only need to serve the 3 files to a browser. If using +[pyscript.com](https://pyscript.com) you only need to ensure the content of the +files, click save then run and view the preview tab. Or, if you are on a machine +with Python installed you can do it from a command line running in the same +directory as the project: + +``` +python -m http.server -b 127.0.0.1 8000 +``` + +This will start a website accessible only to your machine (`-b 127.0.0.1` limits +access only to "localhost" -- your own machine). After running this, you can +visit [http://localhost:8000/](http://localhost:8000/) to run the game in your +browser. + +Congratulations! Now you know the basics of updating games to run in PyScript. +You can continue to develop your game in the typical PyGame-CE way. + +## Running Locally + +Placing an `await` call in the main program script as in the example is not +technically valid Python as it should be in an `async` function. In the +environment executed by PyScript, the code runs in an `async` context so this +works; however, you will notice you cannot run the `quickstart.py` on your +local machine with Python. To fix that, you need to add just a little more +code: + +Place the entire game in a function called `run_game` so that function can be +declared as `async`, allowing it to use `await` in any environment. Import the +`asyncio` package and add the `try ... except` code at the end. Now when running +in the browser, `asyncio.create_task` is used, but when running locally +`asyncio.run` is used. Now you can develop and run locally but also support +publish to the web via PyScript. + +```python +import asyncio +import sys, pygame + +async def run_game(): + pygame.init() + + # Game init ... + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: sys.exit() + + # Game logic ... + + await asyncio.sleep(1/60) + +try: + asyncio.get_running_loop() # succeeds if in async context + asyncio.create_task(run_game()) +except RuntimeError: + asyncio.run(run_game()) # start async context as we're not in one +``` + +!!! Info + + In the web version, the `sys.exit()` was never used because the `QUIT` + event is not generated, but in the local version, responding to the event + is mandatory. + +## Advanced Timing + +While the `await asyncio.sleep(1/60)` is a quick way to approximate 60 FPS, +like all sleep-based timing methods in games this is not precise. Generating +the frame itself takes time, so sleeping 1/60th of a second means total frame +time is longer and actual FPS will be less than 60. + +A better way is to do this is to run your game at the same frame rate as the +display (usually 60, but can be 75, 100, 144, or higher on some displays). When +running in the browser, the proper way to do this is with the JavaScript API +called `requestAnimationFrame`. Using the FFI (foreign function interface) +capabilities of PyScript, we can request the browser's JavaScript runtime to +call the game. The main issue of this method is it requires work to separate the +game setup from the game's execution, which may require more advanced Python +code such as `global` or `class`. However, one benefit is that the `asyncio` +usages are gone. + + +When running locally, you get the same effect from the `vsync=1` parameter on +`pygame.display.set_mode` as `pygame.display.flip()` will pause until the screen +has displayed the frame. In the web version, the `vsync=1` will do nothing, +`flip` will not block, leaving the browser itself to control the timing using +`requestAnimationFrame` by calling `run_one_frame` (via `on_animation_frame`) +each time the display updates. + +Additionally, since frame lengths will be different on each machine, we need to +account for this by creating and using a `dt` (delta time) variable by using a +`pygame.time.Clock`. We update the speed to be in pixels per second and multiply +by `dt` (in seconds) to get the number of pixels to move. + +The code will look like this: + +```python +import sys, pygame + +pygame.init() + +size = width, height = 320, 240 +speed = pygame.Vector2(150, 150) # use Vector2 so we can multiply with dt +black = 0, 0, 0 + +screen = pygame.display.set_mode(size, vsync=1) # Added vsync=1 +ball = pygame.image.load("intro_ball.gif") +ballrect = ball.get_rect() +clock = pygame.time.Clock() # New clock defined + +def run_one_frame(): + for event in pygame.event.get(): + if event.type == pygame.QUIT: sys.exit() + + # in this 300 is for maximum frame rate only, in case vsync is not working + dt = clock.tick(300) / 1000 + + ballrect.move_ip(speed * dt) # use move_ip to avoid the need for "global" + # Remaining game code unchanged ... + + pygame.display.flip() + + +# PyScript-specific code to use requestAnimationFrame in browser +try: + from pyscript import window + from pyscript import ffi + # Running in PyScript + def on_animation_frame(dt): + # For consistency, we use dt from pygame's clock even in browser + run_one_frame() + window.requestAnimationFrame(raf_proxy) + raf_proxy = ffi.create_proxy(on_animation_frame) + on_animation_frame(0) + +except ImportError: + # Local Execution + while True: + run_one_frame() +``` + +A benefit of `vsync` / `requestAnimationFrame` method is that if the game is +running too slowly, frames will naturally be skipped. A drawback is that in the +case of skipped frames and different displays, `dt` will be different. This can +cause problems depending on your game's physics code; the potential solutions +are not unique to the PyScript situation and can be found elsewhere online as an +exercise for the reader. For example, the above example on some machines the +ball will get "stuck" in the sides. In case of issues the `asyncio.sleep` method +without `dt` is easier to deal with for the beginning developer. + +## How it works + +When a `` element is found on the page a +Pyodide instance is bootstrapped with the `pygame-ce` package already included. +Differently from other scripts, `py-game` cannot currently work through a +worker and it uses an optional target attribute to define the `` +element id that will be used to render the game. If no target attribute is +defined, the script assumes there is a `` element already +on the page. + +A config attribute can be specified to add extra packages or bring in additional +files such as images and sounds but right now that's all it can do. + +!!! Info + + Sometimes you need to gather text based user input when starting a game. + The usual way to do this is via the builtin `input` function. + + Because PyGame-CE **only runs on the main thread**, the only way to block + your code while it waits for user `input` is to use a + [JavaScript prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) + instead of input typed in via a terminal. PyScript handles this + automatically for you if you use the `input` function. + +This is an experiment, but: + +* It is possible to use regular PyScript to load the pygame-ce package and use + all the other features. But there be dragons! This helper simply allows + multiple games on a page and forces game logic to run on the main thread to + reduce confusion around attributes and features when the `pygame-ce` package + is meant to be used. Put simply, we make it relatively safe and easy to use. +* The fact `pygame-ce` is the default "game engine" does not mean in the future + PyScript won't have other engines also available. +* Once again, as this is an experiment, we welcome any kind of feedback, + suggestions, hints on how to improve or reports of what's missing. + +Other than that, please go make and share wonderful games. We can't wait to see +what you come up with. diff --git a/docs/user-guide/running-offline.md b/docs/user-guide/running-offline.md new file mode 100644 index 0000000..7e96989 --- /dev/null +++ b/docs/user-guide/running-offline.md @@ -0,0 +1,235 @@ +# Running PyScript Offline + +Although users will want to create and share PyScript apps on the internet, there are cases when user want to run PyScript applications offline, in an airgapped fashion. This means that both PyScript core and the interpreter used to run code need to be served with the application itself. In short, the 2 main explicit tasks needed to create an offline PyScript application are: + +* download and include PyScript core (`core.js`) +* download and include the [Python] interpreters you want to use in your Application + +## Downloading and Including PyScript's `core.js` + +There are at least 2 ways to use PyScript offline: + + * by **cloning the repository**, then building and installing dependencies and then run and then reach the `./dist/` folder + * by **grabbing the npm package** which for simplicity sake will be the method used here at least until we find a better place to *pin* our *dist* folder via our CDN and make the procedure even easier than it is now + +In the examples below, we'll assume we are creating a PyScript Application folder called `pyscript-offline` and we'll add all the necessary files to the folder. + +First of all, we are going to create a `pyscript-offline` folder as reference. + +```sh +mkdir -p pyscript-offline +cd pyscript-offline +``` + +### Adding core by Cloning the Repository + +You can build all the PyScript Core files by cloning the project repository and building them yourself. To do so, build the files by following the instructions in our [developer guide](/developers) + +Once you've run the `build` command, copy the `build` folder that has been created into your `pyscript-offline` folder. + +### Adding core by Installing `@pyscript/core` Locally + +First of all, ensure you are in the folder you would like to test PyScirpt locally. In this case, the `pyscript-offline` folder we created earlier. + +Once within the folder, be sure there is a `package.json` file. Even an empty one with just `{}` as content would work. +This is needed to be sure the folder will include locally the `npm_modules` folder instead of placing the package in the parent folder, if any. + +```sh +# only if there is no package.json, create one +echo '{}' > ./package.json + +# install @pyscript/core +npm i @pyscript/core +``` + +At this point the folder should contain a `node_module` in it and we can actually copy its `dist` folder wherever we like. + +```sh +# create a public folder to serve locally +mkdir -p public + +# move @pyscript/core dist into such folder +cp -R ./node_modules/@pyscript/core/dist ./public/pyscript +``` + +## Setting up your application + +Once you've added PyScript code following one of the methods above, that's almost it! We are half way through our goal but we can already create a `./public/index.html` file that loads the project: + +```html + + + + + + Codestin Search App + + + + + + + +``` + +To run this project directly, after being sure that `index.html` file is saved into the `public` folder, you can try: + +```sh +python3 -m http.server -d ./public/ +``` + +Alternatively, if you would like to test also `worker` features, you can try instead: + +```sh +npx static-handler --coi ./public/ +``` +## Downloading and Setting up a Local Interpreter + +Good news! We are almost there. Now that we've: + +* downloaded PyScript locally +* created the skeleton of an initial PyScript App + +we need to download and setup up an interpreter. PyScript officially supports *MicroPython* and *Pyodide* interpreters, so let's see how to do that for each one of them. + +### Download MicroPython locally + +Similarly to what we did for `@pyscript/core`, we can also install *MicroPython* from *npm*: + +```sh +npm i @micropython/micropython-webassembly-pyscript +``` + +Our `node_modules` folder now should contain a `@micropython` one and from there we can move relevant files into our `public` folder, but let's be sure we have a target for that: + +```sh +# create a folder in our public space +mkdir -p ./public/micropython + +# copy related files into such folder +cp ./node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./public/micropython/ +``` + +That folder should contain at least both `micropython.mjs` and `micropython.wasm` files and these are the files we are going to use locally via our dedicated config. + +```html + + + + + + Codestin Search App + + + + + + interpreter = "/micropython/micropython.mjs" + + + + +``` + +### Install Pyodide locally + +Currently there is a difference between MicroPython and Pyodide: the former does not have (*yet*) a package manager while the latest does, it's called *micropip*. + +This is important to remember because while the procedure to have *pyodide* offline is very similar to the one we've just seen, if we want to use also 3rd party packages we also need to have these running locally ... but let's start simple: + +```sh +# install locally the pyodide module +npm i pyodide + +# create a folder in our public space +mkdir -p ./public/pyodide + +# move all necessary files into that folder +cp ./node_modules/pyodide/pyodide* ./public/pyodide/ +cp ./node_modules/pyodide/python_stdlib.zip ./public/pyodide/ +``` + +Please **note** that also `pyodide-lock.json` file is needed so please don't change that `cp` operation as all `pyodide*` files need to be moved. + +At this point, all we need to do is to change our *HTML* page to use *pyodide* instead: + +```html + + + + + + Codestin Search App + + + + + + interpreter = "/pyodide/pyodide.mjs" + + + + +``` + +## Wrapping it up + +We are basically done! If we try to disconnect from the internet but we still run our local server, the page will still show that very same *Hello from PyScript* message :partying_face: + +We can now drop internet, still keeping the local server running, and everything should be fine :partying_face: + +## Local Pyodide Packages + +There's one last thing that users are probably going to need: the ability to install Python packages when using Pyodide. + +In order to have also 3rd party packages available, we can use the bundle from [pyodide releases](https://github.com/pyodide/pyodide/releases/tag/0.24.1) that contains also packages. + +Please note this bundle is more than 200MB: it not downloaded all at once, it contains each package that is required and it loads only related packages when needed. + +Once downloaded and extracted, where in this case I am using `0.24.1` as reference bundle, we can literally copy and paste, or even move, all those files and folders inside the `pyodide-0.24.1/pyodide/*` directory into our `./public/pyodide/*` folder. + +As the bundle contains files already present, feel free to either skip or replace the content, or even directly move that *pyodide* folder inside our `./public/` one. + +Once it's done, we can now use any package we like that is available in *pyodide*. Let's see an example: + +```html + + + + + + Codestin Search App + + + + + + interpreter = "/pyodide/pyodide.mjs" + packages = ["pandas"] + + + + +``` + +If everything went fine, we should now be able to read `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` on the page *even* if we disconnect from the Internet. + +And **that's all folks** :wave: \ No newline at end of file diff --git a/docs/user-guide/terminal.md b/docs/user-guide/terminal.md new file mode 100644 index 0000000..374d04f --- /dev/null +++ b/docs/user-guide/terminal.md @@ -0,0 +1,197 @@ +# Terminal + +In conventional (non-browser based) Python, it is common to run scripts from +the terminal, or to interact directly with the Python interpreter via the +[REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop). +It's to the terminal that `print` writes characters (via `stdout`), and it's +from the terminal that the `input` reads characters (via `stdin`). + +It usually looks something like this: + + + +Because of the historic importance of the use of a terminal, PyScript makes one +available in the browser (based upon [XTerm.js](https://xtermjs.org/)). +As [mentioned earlier](first-steps.md), PyScript's built-in terminal is +activated with the `terminal` flag when using the ` +``` + +The end result will look like this (the rectangular box indicates the current +position of the cursor): + + + +Should you need an interactive terminal, for example because you use the +`input` statement that requires the user to type things into the terminal, you +**must ensure your code is run on a worker**: + +```html + +``` + + +To use the interactive Python REPL in the terminal, use Python's +[code](https://docs.python.org/3/library/code.html) module like this: + +```python +import code + +code.interact() +``` + +The end result should look something like this: + + + +Finally, it is possible to dynamically pass Python code into the terminal. The +trick is to get a reference to the terminal created by PyScript. Thankfully, +this is very easy. + +Consider this fragment: + +```html + +``` + +Get a reference to the element, and just call the `process` method on +that object: + +```JS +const myterm = document.querySelector("#my_script"); +await myterm.process('print("Hello world!")'); +``` + +## XTerm reference + +Each terminal has a reference to the +[Terminal](https://xtermjs.org/docs/api/terminal/classes/terminal/) +instance used to bootstrap the current terminal. + +On the JavaScript side, it's a `script.terminal` property while on the Python +side, it's a `__terminal__` special reference that guarantees to provide the +very same `script.terminal`: + +```html title="How to reach the XTerm Terminal" + +``` + +### Clear the terminal + +It's very simple to clear a PyTerminal: + +```html title="Clearing the terminal" + +``` + +### Resize the terminal + +The terminal takes up a fair amount of room onscreen. It can be resized to use less. +Here it is 10 lines high. +```python title="Resizing the terminal in python" +if '__terminal__' in locals(): # has a terminal been created + __terminal__.resize(60, 10) # (width, height) +``` + +### Terminal colors + +Colors and most special characters work so you can make the text **bold** or +turn it green. You could even use a control +character to `print('\033[2J')` and clear the terminal, instead of using the +exposed `clear()` method: + +```html title="Terminal colors" + +``` + +### Terminal addons + +It's possible [use XTerm.js addons](https://xtermjs.org/docs/guides/using-addons/): + +```html title="Terminal addons" + + [js_modules.main] + "https://cdn.jsdelivr.net/npm/@xterm/addon-web-links/+esm" = "weblinks" + + +``` + +By default we enable the `WebLinksAddon` addon (so URLs displayed in the +terminal automatically become links). Behind the scenes is the example code +shown above, and this approach will work for +[any other addon](https://github.com/xtermjs/xterm.js/tree/master/addons/) you +may wish to use. + +### MicroPython + +MicroPython has a +[very complete REPL](https://docs.micropython.org/en/latest/reference/repl.html) +already built into it. + + * All `Ctrl+X` strokes are handled, including paste mode and kill switches. + * History works out of the box. Access this via the up and down arrows to + view your command history. + * Tab completion works like a charm. Use the `tab` key to see available + variables or objects in `globals`. + * Copy and paste is much improved. This is true for a single terminal entry, + or a + [paste mode](https://docs.micropython.org/en/latest/reference/repl.html#paste-mode) + enabled variant. + +As a bonus, the MicroPython terminal works on both the main thread and in +web workers, with the following caveats: + +* **Main thread:** + * Calls to the blocking `input` function are delegated to the native browser + based + [prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) + utility. + * There are no guards against blocking code (e.g. `while True:` loops). + Such blocking code _could freeze your page_. +* **Web worker:** + * Conventional support for the `input` function, without blocking the main + thread. + * Blocking code (e.g. `while True:` loops) does not block the main thread + and your page will remain responsive. + +We encourage the usage of `worker` attribute to bootstrap a MicroPython +terminal. But now you have an option to run the terminal in the main thread. +Just remember not to block! diff --git a/docs/user-guide/what.md b/docs/user-guide/what.md new file mode 100644 index 0000000..84304c1 --- /dev/null +++ b/docs/user-guide/what.md @@ -0,0 +1,34 @@ +# What is PyScript? + +[PyScript](https://pyscript.net) is an +[open source](../../license/) platform for +[Python](https://python.org) in the +[browser](https://en.wikipedia.org/wiki/Web_browser). + +PyScript brings together two of the most vibrant technical ecosystems on the +planet. If [the web](https://en.wikipedia.org/wiki/World_Wide_Web) and Python +had a baby, you'd get PyScript. + +PyScript works because modern browsers support +[WebAssembly](https://webassembly.org/) (abbreviated to WASM) - an +[instruction set](https://en.wikipedia.org/wiki/Instruction_set_architecture) +for a [virtual machine](https://en.wikipedia.org/wiki/Virtual_machine) with +an open specification and near native performance. PyScript takes +versions of the Python interpreter compiled to WASM, and makes them easy to use +inside the browser. + +At the core of PyScript is a _philosophy of digital empowerment_. The web is +the world's most ubiquitous computing platform, mature and familiar to billions +of people. Python is one of the +[world's most popular programming languages](https://spectrum.ieee.org/the-top-programming-languages-2023): +it is easy to teach and learn, used in a plethora of existing domains +(such as data science, games, embedded systems, artificial intelligence, +finance, physics and film production - to name but a few), and the Python +ecosystem contains a huge number of popular and powerful libraries to address +its many uses. + +PyScript brings together the ubiquity, familiarity and accessibility of the web +with the power, depth and expressiveness of Python. It means PyScript isn't +just for programming experts but, as we like to say, for the 99% of the rest of +the planet who use computers. + diff --git a/docs/user-guide/workers.md b/docs/user-guide/workers.md index 1df2378..cfc7a8c 100644 --- a/docs/user-guide/workers.md +++ b/docs/user-guide/workers.md @@ -1 +1,347 @@ -# Web Workers +# Workers + +Workers run code that won't block the "main thread" controlling the user +interface. If you block the main thread, your web page becomes annoyingly +unresponsive. **You should never block the main thread.** + +Happily, PyScript makes it very easy to use workers and uses a feature recently +added to web standards called +[Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics). +**You don't need to know about Atomics to use web workers**, but it's useful to +know that the underlying [coincident library](architecture.md#coincident) +uses it under the hood. + +!!! info + + Sometimes you only need to `await` in the main thread on a method in a + worker when neither `window` nor `document` are referenced in the code + running on the worker. + + In these cases, you don't need any special header or service worker + as long as **the method exposed from the worker returns a serializable + result**. + +## HTTP headers + +To use the `window` and `document` objects from within a worker (i.e. use +synchronous Atomics) **you must ensure your web server enables the following +headers** (this is the default behavior for +[pyscript.com](https://pyscript.com)): + +``` +Access-Control-Allow-Origin: * +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: cross-origin +``` + +If you're unable to configure your server's headers, you have two options: + +1. Use the [mini-coi](https://github.com/WebReflection/mini-coi#readme) project + to enforce headers. +2. Use the `service-worker` attribute with the `script` element. + +### Option 1: mini-coi + +For performance reasons, this is the preferred option so Atomics works at +native speed. + +The simplest way to use mini-coi is to copy the +[mini-coi.js](https://raw.githubusercontent.com/WebReflection/mini-coi/main/mini-coi.js) +file content and save it in the root of your website (i.e. `/`), and reference +it as the first child tag in the `` of your HTML documents: + +```html + + + + + + + +``` + +### Option 2: `service-worker` attribute + +This allows you to slot in a custom +[service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) +to handle requirements for synchronous operations. + +Each ` + + +``` + +!!! warning + + Using sabayon as the fallback for synchronous operations via Atomics + should be **the last solution to consider**. It is inevitably + slower than using native Atomics. + + If you must use sabayon, always reduce the amount of synchronous + operations by caching references from the *main* thread. + + ```python + # ❌ THIS IS UNNECESSARILY SLOWER + from pyscript import document + + # add a data-test="not ideal attribute" + document.body.dataset.test = "not ideal" + # read a data-test attribute + print(document.body.dataset.test) + + # - - - - - - - - - - - - - - - - - - - - - + + # ✔️ THIS IS FINE + from pyscript import document + + # if needed elsewhere, reach it once + body = document.body + dataset = body.dataset + + # add a data-test="not ideal attribute" + dataset.test = "not ideal" + # read a data-test attribute + print(dataset.test) + ``` + +In latter example the number of operations has been reduced from six to just +four. The rule of thumb is: _if you ever need a DOM reference more than once, +cache it_. 👍 + + +## Start working + +To start your code in a worker, simply ensure the ` +``` + +You may also want to add a `name` attribute to the tag, so you can use +`pyscript.workers` in the main thread to retrieve a reference to the worker: + +```html + +``` + +```python +from pyscript import workers + +my_worker = await workers["my-worker"] +``` + +Alternatively, to launch a worker from within Python running on the main thread +use the [pyscript.PyWorker](../../api/#pyscriptpyworker) class and you must +reference both the target Python script and interpreter type: + +```python title="Launch a worker from within Python" +from pyscript import PyWorker + +# The type MUST be given and can be either `micropython` or `pyodide` +my_worker = PyWorker("my-worker-code.py", type="micropython") +``` + +## Worker interactions + +Code running in the worker needs to be able to interact with code running in +the main thread and perhaps have access to the web page. This is achieved via +some helpful [builtin APIs](../../api). + +!!! note + + For ease of use, the worker related functionality in PyScript is + a simpler presentation of more sophisticated and powerful behaviour + available via PolyScript. + + **If you are a confident advanced user**, please + [consult the XWorker](https://pyscript.github.io/polyscript/#xworker) + related documentation from the PolyScript project for how to make use of + these features. + +To synchronise serializable data between the worker and the main thread use +[the `sync` function](../../api/#pyscriptsync) in the worker to reference a +function registered on the main thread: + +```python title="Python code running on the main thread." +from pyscript import PyWorker + +def hello(name="world"): + return(f"Hello, {name}") + +# Create the worker. +worker = PyWorker("./worker.py", type="micropython") + +# Register the hello function as callable from the worker. +worker.sync.hello = hello +``` + +```python title="Python code in the resulting worker." +from pyscript import sync, window + +greeting = sync.hello("PyScript") +window.console.log(greeting) +``` + +Alternatively, for the main thread to call functions in a worker, specify the +functions in a `__export__` list: + +```python title="Python code on the worker." +import sys + +def version(): + return sys.version + +# Define what to export to the main thread. +__export__ = ["version", ] +``` + +Then ensure you have a reference to the worker in the main thread (for +instance, by using the `pyscript.workers`): + +```html title="Creating a named worker in the web page." + +``` + +```python title="Referencing and using the worker from the main thread." +from pyscript import workers + +my_worker = await workers["my-worker"] + +print(await my_worker.version()) +``` + +The values passed between the main thread and the worker **must be +serializable**. Try the example given above via +[this project on PyScript.com](https://pyscript.com/@ntoll/tiny-silence/latest). + +No matter if your code is running on the main thread or in a web worker, +both the [`pyscript.window`](../../api/#pyscriptwindow) (representing the main +thread's global window context) and +[`pyscript.document`](../../api/#pyscriptdocument) (representing the web +page's +[document object](https://developer.mozilla.org/en-US/docs/Web/API/Document)) +will be available and work in the same way. As a result, a worker can reach +into the DOM and access some `window` based APIs. + +!!! warning + + Access to the `window` and `document` objects is a powerful feature. Please + remember that: + + * Arguments to and the results from such calls, when used in a worker, + **must be serializable**, otherwise they won't work. + * If you manipulate the DOM via the `document` object, and other workers or + code on the main thread does so too, **they may interfere with each other + and produce unforeseen problematic results**. Remember, with great power + comes great responsibility... and we've given you a bazooka (so please + remember not to shoot yourself in the foot with it). + +## Common Use Case + +While it is possible to start a MicroPython or Pyodide worker from either +MicroPython or Pyodide running on the main thread, the most common use case +we have encountered is MicroPython on the main thread starting a Pyodide +worker. + +Here's how: + +**index.html** +```HTML title="Evaluate main.py via MicroPython on the main thread" + + + + + + + + + + Codestin Search App + + + +``` + +**main.py** +```Python title="MicroPython's main.py: bootstrapping a Pyodide worker." +from pyscript import PyWorker, document + +# Bootstrap the Pyodide worker, with optional config too. +# The worker is: +# * Owned by this script, no JS or Pyodide code in the same page can access +# it. +# * It allows pre-sync methods to be exposed. +# * It has a ready Promise to await for when Pyodide is ready in the worker. +# * It allows the use of post-sync (methods exposed by Pyodide in the +# worker). +worker = PyWorker("worker.py", type="pyodide") + +# Expose a utility that can be immediately invoked in the worker. +worker.sync.greetings = lambda: print("Pyodide bootstrapped") + +print("before ready") +# Await until Pyodide has completed its bootstrap, and is ready. +await worker.ready +print("after ready") + +# Await any exposed methods exposed via Pyodide in the worker. +result = await worker.sync.heavy_computation() +print(result) + +# Show the result at the end of the body. +document.body.append(result) + +# Free memory and get rid of everything in the worker. +worker.terminate() +``` + +**worker.py** +```Python title="The worker.py script runs in the Pyodide worker." +from pyscript import sync + +# Use any methods from main.py on the main thread. +sync.greetings() + +# Expose any methods meant to be used from main. +sync.heavy_computation = lambda: 6 * 7 +``` + +Save these files in a `tmp` folder, ensure [your headers](#http-headers) (just +use `npx mini-coi ./tmp` to serve via localhost) then see the following +outcome in the browser's devtools. + +``` +before ready +Pyodide bootstrapped +after ready +42 +``` diff --git a/mkdocs.yml b/mkdocs.yml index e361549..d62e421 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ site_name: PyScript theme: name: material + custom_dir: overrides logo: assets/images/pyscript-black.svg @@ -51,8 +52,37 @@ extra: provider: mike plugins: + - search - mike: version_selector: true css_dir: css javascript_dir: js canonical_version: null + +nav: + - Home: index.md + - Beginning PyScript: beginning-pyscript.md + - Example Applications: examples.md + - User guide: + - Introduction: user-guide/index.md + - What is PyScript?: user-guide/what.md + - Features: user-guide/features.md + - First steps: user-guide/first-steps.md + - Architecture: user-guide/architecture.md + - Configure PyScript: user-guide/configuration.md + - The DOM & JavaScript: user-guide/dom.md + - Web Workers: user-guide/workers.md + - The FFI in detail: user-guide/ffi.md + - PyScript and filesystems: user-guide/filesystem.md + - Python terminal: user-guide/terminal.md + - Python editor: user-guide/editor.md + - Media: user-guide/media.md + - PyGame-CE: user-guide/pygame-ce.md + - Plugins: user-guide/plugins.md + - Use Offline: user-guide/offline.md + - Built-in APIs: api.md + - FAQ: faq.md + - Contributing: contributing.md + - Developer Guide: developers.md + - Code of Conduct: conduct.md + - License: license.md diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..d4c2203 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block outdated %} + You're not viewing the latest version. + + Click here to go to latest. + +{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..84debdf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +mkdocs-material==9.3.1 +mike==1.1.2 diff --git a/version-update.js b/version-update.js new file mode 100644 index 0000000..c8f6d0b --- /dev/null +++ b/version-update.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +const { join } = require('path'); +const { readdirSync, readFileSync, statSync, writeFileSync } = require('fs'); + +const { version } = require(join(__dirname, 'version.json')); +const calVer = /\/\d{4}\.\d{1,2}\.\d{1,2}\//g; + +const patch = directory => { + for (const file of readdirSync(directory)) { + const path = join(directory, file); + if (file.endsWith('.md')) { + writeFileSync( + path, + readFileSync(path).toString().replace( + calVer, + `/${version}/` + ) + ); + } + else if (statSync(path).isDirectory()) + patch(path); + } +}; + +patch(join(__dirname, 'docs')); diff --git a/version.json b/version.json index 7f29caa..370255b 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2023.09.1" -} \ No newline at end of file + "version": "2025.7.2" +}