@pyscript/core 0.7.11 → 0.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{codemirror-BYspKCDy.js → codemirror-lkW_eC9r.js} +2 -2
- package/dist/codemirror-lkW_eC9r.js.map +1 -0
- package/dist/{codemirror_commands-BLDaEdQ6.js → codemirror_commands-DL2aL4qa.js} +2 -2
- package/dist/codemirror_commands-DL2aL4qa.js.map +1 -0
- package/dist/codemirror_lang-python-DD5EtV36.js +2 -0
- package/dist/codemirror_lang-python-DD5EtV36.js.map +1 -0
- package/dist/codemirror_language-DRHeqAwG.js +2 -0
- package/dist/codemirror_language-DRHeqAwG.js.map +1 -0
- package/dist/codemirror_state-BNbbkoNK.js +2 -0
- package/dist/codemirror_state-BNbbkoNK.js.map +1 -0
- package/dist/codemirror_view-FN7LalDk.js +2 -0
- package/dist/codemirror_view-FN7LalDk.js.map +1 -0
- package/dist/core-B4BRXuDy.js +4 -0
- package/dist/core-B4BRXuDy.js.map +1 -0
- package/dist/core.js +1 -1
- package/dist/{deprecations-manager-DIDxhyRq.js → deprecations-manager-BRHTwqUZ.js} +2 -2
- package/dist/{deprecations-manager-DIDxhyRq.js.map → deprecations-manager-BRHTwqUZ.js.map} +1 -1
- package/dist/{donkey-CLhmQOjG.js → donkey-CBEqGHeD.js} +2 -2
- package/dist/{donkey-CLhmQOjG.js.map → donkey-CBEqGHeD.js.map} +1 -1
- package/dist/{error-uzvvriog.js → error-DRVc1NKK.js} +2 -2
- package/dist/{error-uzvvriog.js.map → error-DRVc1NKK.js.map} +1 -1
- package/dist/index-C-U2wRvV.js +2 -0
- package/dist/index-C-U2wRvV.js.map +1 -0
- package/dist/{mpy-CnF17tqI.js → mpy-B-jI5Qug.js} +2 -2
- package/dist/{mpy-CnF17tqI.js.map → mpy-B-jI5Qug.js.map} +1 -1
- package/dist/{py-BZSSqcx3.js → py-DNLpCVR2.js} +2 -2
- package/dist/{py-BZSSqcx3.js.map → py-DNLpCVR2.js.map} +1 -1
- package/dist/{py-editor-DZ0Dxzzk.js → py-editor-DCtATRBs.js} +2 -2
- package/dist/{py-editor-DZ0Dxzzk.js.map → py-editor-DCtATRBs.js.map} +1 -1
- package/dist/{py-game-bqieV522.js → py-game-BGWt8dH1.js} +2 -2
- package/dist/{py-game-bqieV522.js.map → py-game-BGWt8dH1.js.map} +1 -1
- package/dist/{py-terminal-DYY4WN57.js → py-terminal-BKvzGq7q.js} +2 -2
- package/dist/{py-terminal-DYY4WN57.js.map → py-terminal-BKvzGq7q.js.map} +1 -1
- package/dist/xterm_addon-fit-DxKdSnof.js +14 -0
- package/dist/xterm_addon-fit-DxKdSnof.js.map +1 -0
- package/dist/xterm_addon-web-links-B6rWzrcs.js +14 -0
- package/dist/xterm_addon-web-links-B6rWzrcs.js.map +1 -0
- package/dist/zip-CgZGjqjF.js +2 -0
- package/dist/zip-CgZGjqjF.js.map +1 -0
- package/package.json +15 -14
- package/src/3rd-party/xterm_addon-fit.js +14 -2
- package/src/3rd-party/xterm_addon-web-links.js +14 -2
- package/src/core.js +13 -2
- package/src/stdlib/pyscript/__init__.py +100 -31
- package/src/stdlib/pyscript/context.py +198 -0
- package/src/stdlib/pyscript/display.py +211 -127
- package/src/stdlib/pyscript/events.py +191 -88
- package/src/stdlib/pyscript/fetch.py +156 -25
- package/src/stdlib/pyscript/ffi.py +132 -16
- package/src/stdlib/pyscript/flatted.py +78 -1
- package/src/stdlib/pyscript/fs.py +205 -49
- package/src/stdlib/pyscript/media.py +210 -50
- package/src/stdlib/pyscript/storage.py +214 -27
- package/src/stdlib/pyscript/util.py +28 -7
- package/src/stdlib/pyscript/web.py +1079 -881
- package/src/stdlib/pyscript/websocket.py +252 -45
- package/src/stdlib/pyscript/workers.py +176 -27
- package/src/stdlib/pyscript.js +13 -13
- package/types/stdlib/pyscript.d.ts +1 -1
- package/dist/codemirror-BYspKCDy.js.map +0 -1
- package/dist/codemirror_commands-BLDaEdQ6.js.map +0 -1
- package/dist/codemirror_lang-python-CkOVBHci.js +0 -2
- package/dist/codemirror_lang-python-CkOVBHci.js.map +0 -1
- package/dist/codemirror_language-DOkvasqm.js +0 -2
- package/dist/codemirror_language-DOkvasqm.js.map +0 -1
- package/dist/codemirror_state-BIAL8JKm.js +0 -2
- package/dist/codemirror_state-BIAL8JKm.js.map +0 -1
- package/dist/codemirror_view-Bt4sLgyA.js +0 -2
- package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
- package/dist/core-PTfg6inS.js +0 -4
- package/dist/core-PTfg6inS.js.map +0 -1
- package/dist/index-jZ1aOVVJ.js +0 -2
- package/dist/index-jZ1aOVVJ.js.map +0 -1
- package/dist/xterm_addon-fit--gyF3PcZ.js +0 -2
- package/dist/xterm_addon-fit--gyF3PcZ.js.map +0 -1
- package/dist/xterm_addon-web-links-D95xh2la.js +0 -2
- package/dist/xterm_addon-web-links-D95xh2la.js.map +0 -1
- package/dist/zip-pccs084i.js +0 -2
- package/dist/zip-pccs084i.js.map +0 -1
- package/src/stdlib/pyscript/magic_js.py +0 -84
|
@@ -1,48 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides a unified
|
|
3
|
+
[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface)
|
|
4
|
+
layer for Python/JavaScript interactions, that works consistently across both
|
|
5
|
+
Pyodide and MicroPython, and in a worker or main thread context, abstracting
|
|
6
|
+
away the differences in their JavaScript interop APIs.
|
|
7
|
+
|
|
8
|
+
The following utilities work on both the main thread and in worker contexts:
|
|
9
|
+
|
|
10
|
+
- `create_proxy`: Create a persistent JavaScript proxy of a Python function.
|
|
11
|
+
- `to_js`: Convert Python objects to JavaScript objects.
|
|
12
|
+
- `is_none`: Check if a value is Python `None` or JavaScript `null`.
|
|
13
|
+
- `assign`: Merge objects (like JavaScript's `Object.assign`).
|
|
14
|
+
|
|
15
|
+
The following utilities are specific to worker contexts:
|
|
16
|
+
|
|
17
|
+
- `direct`: Mark objects for direct JavaScript access.
|
|
18
|
+
- `gather`: Collect multiple values from worker contexts.
|
|
19
|
+
- `query`: Query objects in worker contexts.
|
|
20
|
+
|
|
21
|
+
More details of the `direct`, `gather`, and `query` utilities
|
|
22
|
+
[can be found here](https://github.com/WebReflection/reflected-ffi?tab=readme-ov-file#remote-extra-utilities).
|
|
23
|
+
"""
|
|
24
|
+
|
|
1
25
|
try:
|
|
26
|
+
# Attempt to import Pyodide's FFI utilities.
|
|
2
27
|
import js
|
|
3
28
|
from pyodide.ffi import create_proxy as _cp
|
|
4
29
|
from pyodide.ffi import to_js as _py_tjs
|
|
5
30
|
from pyodide.ffi import jsnull
|
|
6
31
|
|
|
7
32
|
from_entries = js.Object.fromEntries
|
|
8
|
-
is_none = lambda value: value is None or value is jsnull
|
|
9
33
|
|
|
10
|
-
def
|
|
11
|
-
if not
|
|
34
|
+
def _to_js_wrapper(value, **kw):
|
|
35
|
+
if "dict_converter" not in kw:
|
|
12
36
|
kw["dict_converter"] = from_entries
|
|
13
37
|
return _py_tjs(value, **kw)
|
|
14
38
|
|
|
15
39
|
except:
|
|
40
|
+
# Fallback to jsffi for MicroPython.
|
|
16
41
|
from jsffi import create_proxy as _cp
|
|
17
|
-
from jsffi import to_js as
|
|
42
|
+
from jsffi import to_js as _to_js_wrapper
|
|
18
43
|
import js
|
|
19
44
|
|
|
20
45
|
jsnull = js.Object.getPrototypeOf(js.Object.prototype)
|
|
21
|
-
is_none = lambda value: value is None or value is jsnull
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
|
|
48
|
+
def create_proxy(func):
|
|
49
|
+
"""
|
|
50
|
+
Create a persistent JavaScript proxy of a Python function.
|
|
51
|
+
|
|
52
|
+
This proxy allows JavaScript code to call the Python function
|
|
53
|
+
seamlessly, maintaining the correct context and argument handling.
|
|
54
|
+
|
|
55
|
+
This is especially useful when passing Python functions as callbacks
|
|
56
|
+
to JavaScript APIs (without `create_proxy`, the function would be
|
|
57
|
+
garbage collected after the declaration of the callback).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pyscript import ffi
|
|
61
|
+
from pyscript import document
|
|
62
|
+
|
|
63
|
+
my_button = document.getElementById("my-button")
|
|
64
|
+
|
|
65
|
+
def py_callback(x):
|
|
66
|
+
print(f"Callback called with {x}")
|
|
67
|
+
|
|
68
|
+
my_button.addEventListener("click", ffi.create_proxy(py_callback))
|
|
69
|
+
```
|
|
70
|
+
"""
|
|
71
|
+
return _cp(func)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def to_js(value, **kw):
|
|
75
|
+
"""
|
|
76
|
+
Convert Python objects to JavaScript objects.
|
|
77
|
+
|
|
78
|
+
This ensures a Python `dict` becomes a
|
|
79
|
+
[proper JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
|
|
80
|
+
rather a JavaScript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map),
|
|
81
|
+
which is more intuitive for most use cases.
|
|
82
|
+
|
|
83
|
+
Where required, the underlying `to_js` uses `Object.fromEntries` for
|
|
84
|
+
`dict` conversion.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pyscript import ffi
|
|
88
|
+
import js
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
note = {
|
|
92
|
+
"body": "This is a notification",
|
|
93
|
+
"icon": "icon.png"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
js.Notification.new("Hello!", ffi.to_js(note))
|
|
97
|
+
```
|
|
98
|
+
"""
|
|
99
|
+
return _to_js_wrapper(value, **kw)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def is_none(value):
|
|
103
|
+
"""
|
|
104
|
+
Check if a value is `None` or JavaScript `null`.
|
|
105
|
+
|
|
106
|
+
In Pyodide, JavaScript `null` is represented by the `jsnull` object,
|
|
107
|
+
so we check for both Python `None` and `jsnull`. This function ensures
|
|
108
|
+
consistent behavior across Pyodide and MicroPython for null-like
|
|
109
|
+
values.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pyscript import ffi
|
|
113
|
+
import js
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
val1 = None
|
|
117
|
+
val2 = js.null
|
|
118
|
+
val3 = 42
|
|
119
|
+
|
|
120
|
+
print(ffi.is_none(val1)) # True
|
|
121
|
+
print(ffi.is_none(val2)) # True
|
|
122
|
+
print(ffi.is_none(val3)) # False
|
|
123
|
+
```
|
|
124
|
+
"""
|
|
125
|
+
return value is None or value is jsnull
|
|
126
|
+
|
|
25
127
|
|
|
26
128
|
try:
|
|
129
|
+
# Worker context utilities from reflected-ffi.
|
|
130
|
+
# See https://github.com/WebReflection/reflected-ffi for more details.
|
|
27
131
|
from polyscript import ffi as _ffi
|
|
28
132
|
|
|
133
|
+
_assign = _ffi.assign
|
|
134
|
+
|
|
29
135
|
direct = _ffi.direct
|
|
30
136
|
gather = _ffi.gather
|
|
31
137
|
query = _ffi.query
|
|
32
138
|
|
|
33
|
-
def assign(source, *args):
|
|
34
|
-
for arg in args:
|
|
35
|
-
_ffi.assign(source, to_js(arg))
|
|
36
|
-
return source
|
|
37
|
-
|
|
38
139
|
except:
|
|
140
|
+
# Fallback implementations for main thread context.
|
|
39
141
|
import js
|
|
40
142
|
|
|
41
143
|
_assign = js.Object.assign
|
|
42
144
|
|
|
43
145
|
direct = lambda source: source
|
|
44
146
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
147
|
+
|
|
148
|
+
def assign(source, *args):
|
|
149
|
+
"""
|
|
150
|
+
Merge JavaScript objects (like
|
|
151
|
+
[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)).
|
|
152
|
+
|
|
153
|
+
Takes a target object and merges properties from one or more source
|
|
154
|
+
objects into it, returning the modified target.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
obj = js.Object.new()
|
|
158
|
+
ffi.assign(obj, {"a": 1}, {"b": 2})
|
|
159
|
+
# obj now has properties a=1 and b=2
|
|
160
|
+
```
|
|
161
|
+
"""
|
|
162
|
+
for arg in args:
|
|
163
|
+
_assign(source, to_js(arg))
|
|
164
|
+
return source
|
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module is a Python implementation of the
|
|
3
|
+
[Flatted JavaScript library](https://www.npmjs.com/package/flatted), which
|
|
4
|
+
provides a light and fast way to serialize and deserialize JSON structures
|
|
5
|
+
that contain circular references.
|
|
6
|
+
|
|
7
|
+
Standard JSON cannot handle circular references - attempting to serialize an
|
|
8
|
+
object that references itself will cause an error. Flatted solves this by
|
|
9
|
+
transforming circular structures into a flat array format that can be safely
|
|
10
|
+
serialized and later reconstructed.
|
|
11
|
+
|
|
12
|
+
Common use cases:
|
|
13
|
+
|
|
14
|
+
- Serializing complex object graphs with circular references.
|
|
15
|
+
- Working with DOM-like structures that contain parent/child references.
|
|
16
|
+
- Preserving object identity when serializing data structures.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from pyscript import flatted
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Create a circular structure.
|
|
23
|
+
obj = {"name": "parent"}
|
|
24
|
+
obj["self"] = obj # Circular reference!
|
|
25
|
+
|
|
26
|
+
# Standard json.dumps would fail here.
|
|
27
|
+
serialized = flatted.stringify(obj)
|
|
28
|
+
|
|
29
|
+
# Reconstruct the original structure.
|
|
30
|
+
restored = flatted.parse(serialized)
|
|
31
|
+
assert restored["self"] is restored # Circular reference preserved!
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
2
34
|
|
|
3
35
|
import json as _json
|
|
4
36
|
|
|
@@ -114,6 +146,26 @@ def _wrap(value):
|
|
|
114
146
|
|
|
115
147
|
|
|
116
148
|
def parse(value, *args, **kwargs):
|
|
149
|
+
"""
|
|
150
|
+
Parse a Flatted JSON string and reconstruct the original structure.
|
|
151
|
+
|
|
152
|
+
This function takes a `value` containing a JSON string created by
|
|
153
|
+
Flatted's stringify() and reconstructs the original Python object,
|
|
154
|
+
including any circular references. The `*args` and `**kwargs` are passed
|
|
155
|
+
to json.loads() for additional customization.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from pyscript import flatted
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Parse a Flatted JSON string.
|
|
162
|
+
json_string = '[{"name": "1", "self": "0"}, "parent"]'
|
|
163
|
+
obj = flatted.parse(json_string)
|
|
164
|
+
|
|
165
|
+
# Circular references are preserved.
|
|
166
|
+
assert obj["self"] is obj
|
|
167
|
+
```
|
|
168
|
+
"""
|
|
117
169
|
json = _json.loads(value, *args, **kwargs)
|
|
118
170
|
wrapped = []
|
|
119
171
|
for value in json:
|
|
@@ -138,6 +190,31 @@ def parse(value, *args, **kwargs):
|
|
|
138
190
|
|
|
139
191
|
|
|
140
192
|
def stringify(value, *args, **kwargs):
|
|
193
|
+
"""
|
|
194
|
+
Serialize a Python object to a Flatted JSON string.
|
|
195
|
+
|
|
196
|
+
This function converts `value`, a Python object (including those with
|
|
197
|
+
circular references), into a JSON string that can be safely transmitted
|
|
198
|
+
or stored. The resulting string can be reconstructed using Flatted's
|
|
199
|
+
parse(). The `*args` and `**kwargs` are passed to json.dumps() for
|
|
200
|
+
additional customization.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from pyscript import flatted
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Create an object with a circular reference.
|
|
207
|
+
parent = {"name": "parent", "children": []}
|
|
208
|
+
child = {"name": "child", "parent": parent}
|
|
209
|
+
parent["children"].append(child)
|
|
210
|
+
|
|
211
|
+
# Serialize it (standard json.dumps would fail here).
|
|
212
|
+
json_string = flatted.stringify(parent)
|
|
213
|
+
|
|
214
|
+
# Can optionally pretty-print via JSON indentation etc.
|
|
215
|
+
pretty = flatted.stringify(parent, indent=2)
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
141
218
|
known = _Known()
|
|
142
219
|
input = []
|
|
143
220
|
output = []
|
|
@@ -1,7 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides an API for mounting directories from the user's local
|
|
3
|
+
filesystem into the browser's virtual filesystem. This means Python code,
|
|
4
|
+
running in the browser, can read and write files on the user's local machine.
|
|
5
|
+
|
|
6
|
+
!!! warning
|
|
7
|
+
**This API only works in Chromium-based browsers** (Chrome, Edge,
|
|
8
|
+
Vivaldi, Brave, etc.) that support the
|
|
9
|
+
[File System Access API](https://wicg.github.io/file-system-access/).
|
|
10
|
+
|
|
11
|
+
The module maintains a `mounted` dictionary that tracks all currently mounted
|
|
12
|
+
paths and their associated filesystem handles.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from pyscript import fs, document, when
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Mount a local directory to the `/local` mount point in the browser's
|
|
19
|
+
# virtual filesystem (may prompt user for permission).
|
|
20
|
+
await fs.mount("/local")
|
|
21
|
+
|
|
22
|
+
# Alternatively, mount on a button click event. This is important because
|
|
23
|
+
# if the call to `fs.mount` happens after a click or other transient event,
|
|
24
|
+
# the confirmation dialog will not be shown.
|
|
25
|
+
@when("click", "#mount-button")
|
|
26
|
+
async def handler(event):
|
|
27
|
+
await fs.mount("/another_dir")
|
|
28
|
+
|
|
29
|
+
# Work with files in the mounted directory as usual.
|
|
30
|
+
with open("/local/example.txt", "w") as f:
|
|
31
|
+
f.write("Hello from PyScript!")
|
|
32
|
+
|
|
33
|
+
# Ensure changes are written to local filesystem.
|
|
34
|
+
await fs.sync("/local")
|
|
35
|
+
|
|
36
|
+
# Clean up when done.
|
|
37
|
+
await fs.unmount("/local")
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import js
|
|
42
|
+
from _pyscript import fs as _fs, interpreter
|
|
43
|
+
from pyscript import window
|
|
44
|
+
from pyscript.ffi import to_js
|
|
45
|
+
from pyscript.context import RUNNING_IN_WORKER
|
|
46
|
+
|
|
47
|
+
# Worker-specific imports.
|
|
48
|
+
if RUNNING_IN_WORKER:
|
|
49
|
+
from pyscript.context import sync as sync_with_worker
|
|
50
|
+
from polyscript import IDBMap
|
|
51
|
+
|
|
1
52
|
mounted = {}
|
|
53
|
+
"""Global dictionary tracking mounted paths and their filesystem handles."""
|
|
2
54
|
|
|
3
55
|
|
|
4
|
-
async def
|
|
56
|
+
async def _check_permission(details):
|
|
57
|
+
"""
|
|
58
|
+
Check if permission has been granted for a filesystem handler. Returns
|
|
59
|
+
the handler if permission is granted, otherwise None.
|
|
60
|
+
"""
|
|
5
61
|
handler = details.handler
|
|
6
62
|
options = details.options
|
|
7
63
|
permission = await handler.queryPermission(options)
|
|
@@ -9,94 +65,194 @@ async def get_handler(details):
|
|
|
9
65
|
|
|
10
66
|
|
|
11
67
|
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
|
12
|
-
|
|
13
|
-
from
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
68
|
+
"""
|
|
69
|
+
Mount a directory from the local filesystem to the virtual filesystem
|
|
70
|
+
at the specified `path` mount point. The `mode` can be "readwrite" or
|
|
71
|
+
"read" to specify access level. The `root` parameter provides a hint
|
|
72
|
+
for the file picker starting location. The `id` parameter allows multiple
|
|
73
|
+
distinct mounts at the same path.
|
|
74
|
+
|
|
75
|
+
On first use, the browser will prompt the user to select a directory
|
|
76
|
+
and grant permission.
|
|
19
77
|
|
|
78
|
+
```python
|
|
79
|
+
from pyscript import fs
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Basic mount with default settings.
|
|
83
|
+
await fs.mount("/local")
|
|
84
|
+
|
|
85
|
+
# Mount with read-only access.
|
|
86
|
+
await fs.mount("/readonly", mode="read")
|
|
87
|
+
|
|
88
|
+
# Mount with a hint to start in Downloads folder.
|
|
89
|
+
await fs.mount("/downloads", root="downloads")
|
|
90
|
+
|
|
91
|
+
# Mount with a custom ID to track different directories.
|
|
92
|
+
await fs.mount("/project", id="my-project")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If called during a user interaction (like a button click), the
|
|
96
|
+
permission dialog may be skipped if permission was previously granted.
|
|
97
|
+
"""
|
|
20
98
|
js.console.warn("experimental pyscript.fs ⚠️")
|
|
21
99
|
|
|
100
|
+
# Check if path is already mounted with a different ID.
|
|
101
|
+
mount_key = f"{path}@{id}"
|
|
102
|
+
if path in mounted:
|
|
103
|
+
# Path already mounted - check if it's the same ID.
|
|
104
|
+
for existing_key in mounted.keys():
|
|
105
|
+
if existing_key.startswith(f"{path}@") and existing_key != mount_key:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Path '{path}' is already mounted with a different ID. "
|
|
108
|
+
f"Unmount it first or use a different path."
|
|
109
|
+
)
|
|
110
|
+
|
|
22
111
|
details = None
|
|
23
112
|
handler = None
|
|
24
113
|
|
|
25
|
-
uid = f"{path}@{id}"
|
|
26
|
-
|
|
27
114
|
options = {"id": id, "mode": mode}
|
|
28
115
|
if root != "":
|
|
29
116
|
options["startIn"] = root
|
|
30
117
|
|
|
31
118
|
if RUNNING_IN_WORKER:
|
|
32
|
-
|
|
119
|
+
fs_handler = sync_with_worker.storeFSHandler(mount_key, to_js(options))
|
|
33
120
|
|
|
34
|
-
#
|
|
35
|
-
if isinstance(
|
|
36
|
-
success =
|
|
121
|
+
# Handle both async and SharedArrayBuffer use cases.
|
|
122
|
+
if isinstance(fs_handler, bool):
|
|
123
|
+
success = fs_handler
|
|
37
124
|
else:
|
|
38
|
-
success = await
|
|
125
|
+
success = await fs_handler
|
|
39
126
|
|
|
40
127
|
if success:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
idbm = IDBMap.new(fs.NAMESPACE)
|
|
45
|
-
details = await idbm.get(uid)
|
|
46
|
-
handler = await get_handler(details)
|
|
128
|
+
idbm = IDBMap.new(_fs.NAMESPACE)
|
|
129
|
+
details = await idbm.get(mount_key)
|
|
130
|
+
handler = await _check_permission(details)
|
|
47
131
|
if handler is None:
|
|
48
|
-
#
|
|
49
|
-
await js.Promise.resolve(
|
|
132
|
+
# Force await in either async or sync scenario.
|
|
133
|
+
await js.Promise.resolve(sync_with_worker.getFSHandler(details.options))
|
|
50
134
|
handler = details.handler
|
|
51
|
-
|
|
52
135
|
else:
|
|
53
|
-
raise RuntimeError(
|
|
136
|
+
raise RuntimeError(_fs.ERROR)
|
|
54
137
|
|
|
55
138
|
else:
|
|
56
|
-
success = await
|
|
139
|
+
success = await _fs.idb.has(mount_key)
|
|
57
140
|
|
|
58
141
|
if success:
|
|
59
|
-
details = await
|
|
60
|
-
handler = await
|
|
142
|
+
details = await _fs.idb.get(mount_key)
|
|
143
|
+
handler = await _check_permission(details)
|
|
61
144
|
if handler is None:
|
|
62
|
-
handler = await
|
|
145
|
+
handler = await _fs.getFileSystemDirectoryHandle(details.options)
|
|
63
146
|
else:
|
|
64
147
|
js_options = to_js(options)
|
|
65
|
-
handler = await
|
|
148
|
+
handler = await _fs.getFileSystemDirectoryHandle(js_options)
|
|
66
149
|
details = {"handler": handler, "options": js_options}
|
|
67
|
-
await
|
|
150
|
+
await _fs.idb.set(mount_key, to_js(details))
|
|
68
151
|
|
|
69
152
|
mounted[path] = await interpreter.mountNativeFS(path, handler)
|
|
70
153
|
|
|
71
154
|
|
|
72
|
-
async def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
RUNNING_IN_WORKER,
|
|
76
|
-
sync,
|
|
77
|
-
)
|
|
155
|
+
async def sync(path):
|
|
156
|
+
"""
|
|
157
|
+
Synchronise the virtual and local filesystems for a mounted `path`.
|
|
78
158
|
|
|
79
|
-
|
|
159
|
+
This ensures all changes made in the browser's virtual filesystem are
|
|
160
|
+
written to the user's local filesystem, and vice versa.
|
|
80
161
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
else:
|
|
84
|
-
had = await fs.idb.has(uid)
|
|
85
|
-
if had:
|
|
86
|
-
had = await fs.idb.delete(uid)
|
|
162
|
+
```python
|
|
163
|
+
from pyscript import fs
|
|
87
164
|
|
|
88
|
-
if had:
|
|
89
|
-
interpreter._module.FS.unmount(path)
|
|
90
165
|
|
|
91
|
-
|
|
166
|
+
await fs.mount("/local")
|
|
92
167
|
|
|
168
|
+
# Make changes to files.
|
|
169
|
+
with open("/local/data.txt", "w") as f:
|
|
170
|
+
f.write("Important data")
|
|
93
171
|
|
|
94
|
-
|
|
172
|
+
# Ensure changes are written to local disk.
|
|
173
|
+
await fs.sync("/local")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
This is automatically called by unmount(), but you may want to call
|
|
177
|
+
it explicitly to ensure data persistence at specific points.
|
|
178
|
+
"""
|
|
179
|
+
if path not in mounted:
|
|
180
|
+
raise KeyError(
|
|
181
|
+
f"Path '{path}' is not mounted. " f"Use fs.mount() to mount it first."
|
|
182
|
+
)
|
|
95
183
|
await mounted[path].syncfs()
|
|
96
184
|
|
|
97
185
|
|
|
98
186
|
async def unmount(path):
|
|
99
|
-
|
|
187
|
+
"""
|
|
188
|
+
Unmount a directory, specified by `path`, from the virtual filesystem.
|
|
189
|
+
|
|
190
|
+
This synchronises any pending changes and then removes the mount point,
|
|
191
|
+
freeing up memory. The `path` can be reused for mounting a different
|
|
192
|
+
directory.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from pyscript import fs
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
await fs.mount("/local")
|
|
199
|
+
# ... work with files ...
|
|
200
|
+
await fs.unmount("/local")
|
|
201
|
+
|
|
202
|
+
# Path can now be reused.
|
|
203
|
+
await fs.mount("/local", id="different-folder")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This automatically calls `sync()` before unmounting to ensure no data
|
|
207
|
+
is lost.
|
|
208
|
+
"""
|
|
209
|
+
if path not in mounted:
|
|
210
|
+
raise KeyError(f"Path '{path}' is not mounted. Cannot unmount.")
|
|
100
211
|
|
|
101
212
|
await sync(path)
|
|
102
213
|
interpreter._module.FS.unmount(path)
|
|
214
|
+
del mounted[path]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def revoke(path, id="pyscript"):
|
|
218
|
+
"""
|
|
219
|
+
Revoke filesystem access permission and unmount for a given
|
|
220
|
+
`path` and `id` combination.
|
|
221
|
+
|
|
222
|
+
This removes the stored permission for accessing the user's local
|
|
223
|
+
filesystem at the specified path and ID. Unlike `unmount()`, which only
|
|
224
|
+
removes the mount point, `revoke()` also clears the permission so the
|
|
225
|
+
user will be prompted again on next mount.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from pyscript import fs
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
await fs.mount("/local", id="my-app")
|
|
232
|
+
# ... work with files ...
|
|
233
|
+
|
|
234
|
+
# Revoke permission (user will be prompted again next time).
|
|
235
|
+
revoked = await fs.revoke("/local", id="my-app")
|
|
236
|
+
|
|
237
|
+
if revoked:
|
|
238
|
+
print("Permission revoked successfully")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
After revoking, the user will need to grant permission again and
|
|
242
|
+
select a directory when `mount()` is called next time.
|
|
243
|
+
"""
|
|
244
|
+
mount_key = f"{path}@{id}"
|
|
245
|
+
|
|
246
|
+
if RUNNING_IN_WORKER:
|
|
247
|
+
handler_exists = sync_with_worker.deleteFSHandler(mount_key)
|
|
248
|
+
else:
|
|
249
|
+
handler_exists = await _fs.idb.has(mount_key)
|
|
250
|
+
if handler_exists:
|
|
251
|
+
handler_exists = await _fs.idb.delete(mount_key)
|
|
252
|
+
|
|
253
|
+
if handler_exists:
|
|
254
|
+
interpreter._module.FS.unmount(path)
|
|
255
|
+
if path in mounted:
|
|
256
|
+
del mounted[path]
|
|
257
|
+
|
|
258
|
+
return handler_exists
|