diff --git a/docs/api.md b/docs/api.md index e5dceb3..869535f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -331,6 +331,86 @@ 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). diff --git a/docs/beginning-pyscript.md b/docs/beginning-pyscript.md index d153bdf..38da6ac 100644 --- a/docs/beginning-pyscript.md +++ b/docs/beginning-pyscript.md @@ -117,8 +117,8 @@ module in the document's `` tag: 🦜 Polyglot - Piratical PyScript - - + + @@ -168,8 +168,8 @@ In the end, our HTML should look like this: 🦜 Polyglot - Piratical PyScript - - + +

Polyglot 🦜 💬 🇬🇧 ➡️ 🏴‍☠️

diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index f036330..7766403 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -121,9 +121,11 @@ version of Pyodide as specified in the previous examples: ### Files -The `files` option fetches arbitrary content from URLs onto the filesystem -available to Python, and emulated by the browser. Just map a valid URL to a -destination filesystem path. +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: diff --git a/docs/user-guide/filesystem.md b/docs/user-guide/filesystem.md new file mode 100644 index 0000000..a7c824b --- /dev/null +++ b/docs/user-guide/filesystem.md @@ -0,0 +1,175 @@ +# 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**. + + 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 index 7fcd843..abe37f8 100644 --- a/docs/user-guide/first-steps.md +++ b/docs/user-guide/first-steps.md @@ -20,9 +20,9 @@ CSS: - + - + diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md index baf094e..7aed1cb 100644 --- a/docs/user-guide/plugins.md +++ b/docs/user-guide/plugins.md @@ -100,7 +100,7 @@ For example, this will work because all references are contained within the registered function: ```js -import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js"; +import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js"; hooks.worker.onReady.add(() => { // NOT suggested, just an example! @@ -114,7 +114,7 @@ hooks.worker.onReady.add(() => { However, due to the outer reference to the variable `i`, this will fail: ```js -import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js"; +import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js"; // NO NO NO NO NO! ☠️ let i = 0; @@ -147,7 +147,7 @@ the page. ```js title="log.js - a plugin that simply logs to the console." // import the hooks from PyScript first... -import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js"; +import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js"; // The `hooks.main` attribute defines plugins that run on the main thread. hooks.main.onReady.add((wrap, element) => { @@ -197,8 +197,8 @@ hooks.worker.onAfterRun.add(() => { - - + + + PyWorker - mpy bootstrapping pyodide example diff --git a/mkdocs.yml b/mkdocs.yml index 44eb83a..3757deb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - 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 - PyGame-CE: user-guide/pygame-ce.md diff --git a/version.json b/version.json index d05966c..142d482 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2025.2.2" + "version": "2025.2.3" }