Skip to content

Commit 0202ed3

Browse files
authored
Merge pull request #169 from pyscript/2025-2-3
2025 2 3
2 parents e52c350 + f4daf4d commit 0202ed3

9 files changed

+275
-17
lines changed

Diff for: docs/api.md

+80
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,86 @@ is explicitly specified and the runtime is Pyodide.
331331

332332
The technical details of how this works are [described here](../user-guide/ffi#to_js).
333333

334+
### `pyscript.fs`
335+
336+
!!! danger
337+
338+
This API only works in Chromium based browsers.
339+
340+
An API for mounting the user's local filesystem to a designated directory in
341+
the browser's virtual filesystem. Please see
342+
[the filesystem](../user-guide/filesystem) section of the user-guide for more
343+
information.
344+
345+
#### `pyscript.fs.mount`
346+
347+
Mount a directory on the user's local filesystem into the browser's virtual
348+
filesystem. If no previous
349+
[transient user activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation)
350+
has taken place, this function will result in a minimalist dialog to provide
351+
the required transient user activation.
352+
353+
This asynchronous function takes four arguments:
354+
355+
* `path` (required) - indicating the location on the in-browser filesystem to
356+
which the user selected directory from the local filesystem will be mounted.
357+
* `mode` (default: `"readwrite"`) - indicates how the code may interact with
358+
the mounted filesystem. May also be just `"read"` for read-only access.
359+
* `id` (default: `"pyscript"`) - indicate a unique name for the handler
360+
associated with a directory on the user's local filesystem. This allows users
361+
to select different folders and mount them at the same path in the
362+
virtual filesystem.
363+
* `root` (default: `""`) - a hint to the browser for where to start picking the
364+
path that should be mounted in Python. Valid values are: `desktop`,
365+
`documents`, `downloads`, `music`, `pictures` or `videos` as per
366+
[web standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#startin).
367+
368+
```python title="Mount a local directory to the '/local' directory in the browser's virtual filesystem"
369+
from pyscript import fs
370+
371+
372+
# May ask for permission from the user, and select the local target.
373+
await fs.mount("/local")
374+
```
375+
376+
If the call to `fs.mount` happens after a click or other transient event, the
377+
confirmation dialog will not be shown.
378+
379+
```python title="Mounting without a transient event dialog."
380+
from pyscript import fs
381+
382+
383+
async def handler(event):
384+
"""
385+
The click event that calls this handler is already a transient event.
386+
"""
387+
await fs.mount("/local")
388+
389+
390+
my_button.onclick = handler
391+
```
392+
393+
#### `pyscript.fs.sync`
394+
395+
Given a named `path` for a mount point on the browser's virtual filesystem,
396+
asynchronously ensure the virtual and local directories are synchronised (i.e.
397+
all changes made in the browser's mounted filesystem, are propagated to the
398+
user's local filesystem).
399+
400+
```python title="Synchronise the virtual and local filesystems."
401+
await fs.sync("/local")
402+
```
403+
404+
#### `pyscript.fs.unmount`
405+
406+
Asynchronously unmount the named `path` from the browser's virtual filesystem
407+
after ensuring content is synchronized. This will free up memory and allow you
408+
to re-use the path to mount a different directory.
409+
410+
```python title="Unmount from the virtual filesystem."
411+
await fs.unmount("/local")
412+
```
413+
334414
### `pyscript.js_modules`
335415

336416
It is possible to [define JavaScript modules to use within your Python code](../user-guide/configuration#javascript-modules).

Diff for: docs/beginning-pyscript.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ module in the document's `<head>` tag:
117117
<meta charset="utf-8" />
118118
<meta name="viewport" content="width=device-width,initial-scale=1" />
119119
<title>🦜 Polyglot - Piratical PyScript</title>
120-
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.2/core.css">
121-
<script type="module" src="https://pyscript.net/releases/2025.2.2/core.js"></script>
120+
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.3/core.css">
121+
<script type="module" src="https://pyscript.net/releases/2025.2.3/core.js"></script>
122122
</head>
123123
<body>
124124

@@ -168,8 +168,8 @@ In the end, our HTML should look like this:
168168
<meta charset="utf-8" />
169169
<meta name="viewport" content="width=device-width,initial-scale=1" />
170170
<title>🦜 Polyglot - Piratical PyScript</title>
171-
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.2/core.css">
172-
<script type="module" src="https://pyscript.net/releases/2025.2.2/core.js"></script>
171+
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.3/core.css">
172+
<script type="module" src="https://pyscript.net/releases/2025.2.3/core.js"></script>
173173
</head>
174174
<body>
175175
<h1>Polyglot 🦜 💬 🇬🇧 ➡️ 🏴‍☠️</h1>

Diff for: docs/user-guide/configuration.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ version of Pyodide as specified in the previous examples:
121121

122122
### Files
123123

124-
The `files` option fetches arbitrary content from URLs onto the filesystem
125-
available to Python, and emulated by the browser. Just map a valid URL to a
126-
destination filesystem path.
124+
The `files` option fetches arbitrary content from URLs onto the virtual
125+
filesystem available to Python, and emulated by the browser. Just map a valid
126+
URL to a destination filesystem path on the in-browser virtual filesystem. You
127+
can find out more in the section about
128+
[PyScript and filesystems](../filesystem/).
127129

128130
The following JSON and TOML are equivalent:
129131

Diff for: docs/user-guide/filesystem.md

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# PyScript and Filesystems
2+
3+
As you know, the filesystem is where you store files. For Python to work there
4+
needs to be a filesystem in which Python packages, modules and data for your
5+
apps can be found. When you `import` a library, or when you `open` a file, it
6+
is on the in-browser virtual filesystem that Python looks.
7+
8+
However, things are not as they may seem.
9+
10+
This section clarifies what PyScript means by a filesystem, and the way in
11+
which PyScript interacts with such a concept.
12+
13+
## Two filesystems
14+
15+
PyScript interacts with two filesystems.
16+
17+
1. The browser, thanks to
18+
[Emscripten](https://emscripten.org/docs/api_reference/Filesystem-API.html),
19+
provides a virtual in-memory filesystem. **This has nothing to do with your
20+
device's local filesystem**, but is contained within the browser based
21+
sandbox used by PyScript. The [files](../configuration/#files)
22+
configuration API defines what is found on this filesystem.
23+
2. PyScript provides an easy to use API for accessing your device's local
24+
filesystem. It requires permission from the user to mount a folder from the
25+
local filesystem onto a directory in the browser's virtual filesystem. Think
26+
of it as gate-keeping a bridge to the outside world of the device's local
27+
filesystem.
28+
29+
!!! danger
30+
31+
Access to the device's local filesystem **is only available in Chromium
32+
based browsers**.
33+
34+
Firefox and Safari do not support this capability (yet), and so it is not
35+
available to PyScript running in these browsers.
36+
37+
## The in-browser filesystem
38+
39+
The filesystem that both Pyodide and MicroPython use by default is the
40+
[in-browser virtual filesystem](https://emscripten.org/docs/api_reference/Filesystem-API.html).
41+
Opening files and importing modules takes place in relation to this sandboxed
42+
environment, configured via the [files](../configuration/#files) entry in your
43+
settings.
44+
45+
```toml title="Filesystem configuration via TOML."
46+
[files]
47+
"https://example.com/myfile.txt": ""
48+
```
49+
50+
```python title="Just use the resulting file 'as usual'."
51+
# Interacting with the virtual filesystem, "as usual".
52+
with open("myfile.txt", "r") as myfile:
53+
print(myfile.read())
54+
```
55+
56+
Currently, each time you re-load the page, the filesystem is recreated afresh,
57+
so any data stored by PyScript to this filesystem will be lost.
58+
59+
!!! info
60+
61+
In the future, we may make it possible to configure the in-browser virtual
62+
filesystem as persistent across re-loads.
63+
64+
[This article](https://emscripten.org/docs/porting/files/file_systems_overview.html)
65+
gives an excellent overview of the browser based virtual filesystem's
66+
implementation and architecture.
67+
68+
The most important key concepts to remember are:
69+
70+
* The PyScript filesystem is contained *within* the browser's sandbox.
71+
* Each instance of a Python interpreter used by PyScript runs in a separate
72+
sandbox, and so does NOT share virtual filesystems.
73+
* All Python related filesytem operations work as expected with this
74+
filesystem.
75+
* The virtual filesystem is configured via the
76+
[files](../configuration/#files) entry in your settings.
77+
* The virtual filesystem is (currently) NOT persistent between page re-loads.
78+
* Currently, the filesystem has a maximum capacity of 4GB of data (something
79+
over which we have no control).
80+
81+
## The device's local filesystem
82+
83+
**Access to the device's local filesystem currently only works on Chromium
84+
based browsers**.
85+
86+
Your device (the laptop, mobile or tablet) that runs your browser has a
87+
filesystem provided by a hard drive. Thanks to the
88+
[`pyscript.fs` namespace in our API](../../api/#pyscriptfs), both MicroPython
89+
and Pyodide (CPython) gain access to this filesystem should the user of
90+
your code allow this to happen.
91+
92+
This is a [transient activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation)
93+
for the purposes of
94+
[user activation of gated features](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation).
95+
Put simply, before your code gains access to their local filesystem, an
96+
explicit agreement needs to be gathered from the user. Part of this process
97+
involves asking the user to select a target directory on their local
98+
filesystem, to which PyScript will be given access.
99+
100+
The directory on their local filesystem, selected by the user, is then mounted
101+
to a given directory inside the browser's virtual filesystem. In this way a
102+
mapping is made between the sandboxed world of the browser, and the outside
103+
world of the user's filesystem.
104+
105+
Your code will then be able to perform all the usual filesystem related
106+
operations provided by Python, within the mounted directory. However, **such
107+
changes will NOT take effect on the local filesystem UNTIL your code
108+
explicitly calls the `sync` function**. At this point, the state of the
109+
in-browser virtual filesystem and the user's local filesystem are synchronised.
110+
111+
The following code demonstrates the simplest use case:
112+
113+
```python title="The core operations of the pyscript.fs API"
114+
from pyscript import fs
115+
116+
# Ask once for permission to mount any local folder
117+
# into the virtual filesystem handled by Pyodide/MicroPython.
118+
# The folder "/local" refers to the directory on the virtual
119+
# filesystem to which the user-selected directory will be
120+
# mounted.
121+
await fs.mount("/local")
122+
123+
# ... DO FILE RELATED OPERATIONS HERE ...
124+
125+
# If changes were made, ensure these are persisted to the local filesystem's
126+
# folder.
127+
await fs.sync("/local")
128+
129+
# If needed to free RAM or that specific path, sync and unmount
130+
await fs.unmount("/local")
131+
```
132+
133+
It is possible to use multiple different local directories with the same mount
134+
point. This is important if your application provides some generic
135+
functionality on data that might be in different local directories because
136+
while the nature of the data might be similar, the subject is not. For
137+
instance, you may have different models for a PyScript based LLM in different
138+
directories, and may wish to switch between them at runtime using different
139+
handlers (requiring their own transient action). In which case use
140+
the following technique:
141+
142+
```python title="Multiple local directories on the same mount point"
143+
# Mount a local folder specifying a different handler.
144+
# This requires a user explicit transient action (once).
145+
await fs.mount("/local", id="v1")
146+
# ... operate on that folder ...
147+
await fs.unmount("/local")
148+
149+
# Mount a local folder specifying a different handler.
150+
# This also requires a user explicit transient action (once).
151+
await fs.mount("/local", id="v2")
152+
# ... operate on that folder ...
153+
await fs.unmount("/local")
154+
155+
# Go back to the original handler or a previous one.
156+
# No transient action required now.
157+
await fs.mount("/local", id="v1")
158+
# ... operate again on that folder ...
159+
```
160+
161+
In addition to the mount `path` and handler `id`, the `fs.mount` function can
162+
take two further arguments:
163+
164+
* `mode` (by default `"readwrite"`) indicates the sort of activity available to
165+
the user. It can also be set to `read` for read-only access to the local
166+
filesystem. This is a part of the
167+
[web-standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#mode)
168+
for directory selection.
169+
* `root` - (by default, `""`) is a hint to the browser for where to start
170+
picking the path that should be mounted in Python. Valid values are:
171+
`desktop`, `documents`, `downloads`, `music`, `pictures` or `videos`
172+
[as per web standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#startin).
173+
174+
The `sync` and `unmount` functions only accept the mount `path` used in the
175+
browser's local filesystem.

Diff for: docs/user-guide/first-steps.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ CSS:
2020
<meta charset="UTF-8">
2121
<meta name="viewport" content="width=device-width,initial-scale=1.0">
2222
<!-- PyScript CSS -->
23-
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.2/core.css">
23+
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.3/core.css">
2424
<!-- This script tag bootstraps PyScript -->
25-
<script type="module" src="https://pyscript.net/releases/2025.2.2/core.js"></script>
25+
<script type="module" src="https://pyscript.net/releases/2025.2.3/core.js"></script>
2626
</head>
2727
<body>
2828
<!-- your code goes here... -->

Diff for: docs/user-guide/plugins.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ For example, this will work because all references are contained within the
100100
registered function:
101101

102102
```js
103-
import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js";
103+
import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js";
104104

105105
hooks.worker.onReady.add(() => {
106106
// NOT suggested, just an example!
@@ -114,7 +114,7 @@ hooks.worker.onReady.add(() => {
114114
However, due to the outer reference to the variable `i`, this will fail:
115115

116116
```js
117-
import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js";
117+
import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js";
118118

119119
// NO NO NO NO NO! ☠️
120120
let i = 0;
@@ -147,7 +147,7 @@ the page.
147147

148148
```js title="log.js - a plugin that simply logs to the console."
149149
// import the hooks from PyScript first...
150-
import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js";
150+
import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js";
151151

152152
// The `hooks.main` attribute defines plugins that run on the main thread.
153153
hooks.main.onReady.add((wrap, element) => {
@@ -197,8 +197,8 @@ hooks.worker.onAfterRun.add(() => {
197197
<!-- JS plugins should be available before PyScript bootstraps -->
198198
<script type="module" src="./log.js"></script>
199199
<!-- PyScript -->
200-
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.2/core.css">
201-
<script type="module" src="https://pyscript.net/releases/2025.2.2/core.js"></script>
200+
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.3/core.css">
201+
<script type="module" src="https://pyscript.net/releases/2025.2.3/core.js"></script>
202202
</head>
203203
<body>
204204
<script type="mpy">

Diff for: docs/user-guide/workers.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,9 @@ Here's how:
282282
<meta charset="utf-8">
283283
<meta name="viewport" content="width=device-width,initial-scale=1">
284284
<!-- PyScript CSS -->
285-
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.2/core.css">
285+
<link rel="stylesheet" href="https://pyscript.net/releases/2025.2.3/core.css">
286286
<!-- This script tag bootstraps PyScript -->
287-
<script type="module" src="https://pyscript.net/releases/2025.2.2/core.js"></script>
287+
<script type="module" src="https://pyscript.net/releases/2025.2.3/core.js"></script>
288288
<title>PyWorker - mpy bootstrapping pyodide example</title>
289289
<script type="mpy" src="main.py"></script>
290290
</head>

Diff for: mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ nav:
7272
- The DOM &amp; JavaScript: user-guide/dom.md
7373
- Web Workers: user-guide/workers.md
7474
- The FFI in detail: user-guide/ffi.md
75+
- PyScript and filesystems: user-guide/filesystem.md
7576
- Python terminal: user-guide/terminal.md
7677
- Python editor: user-guide/editor.md
7778
- PyGame-CE: user-guide/pygame-ce.md

Diff for: version.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"version": "2025.2.2"
2+
"version": "2025.2.3"
33
}

0 commit comments

Comments
 (0)