@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.
Files changed (80) hide show
  1. package/dist/{codemirror-BYspKCDy.js → codemirror-lkW_eC9r.js} +2 -2
  2. package/dist/codemirror-lkW_eC9r.js.map +1 -0
  3. package/dist/{codemirror_commands-BLDaEdQ6.js → codemirror_commands-DL2aL4qa.js} +2 -2
  4. package/dist/codemirror_commands-DL2aL4qa.js.map +1 -0
  5. package/dist/codemirror_lang-python-DD5EtV36.js +2 -0
  6. package/dist/codemirror_lang-python-DD5EtV36.js.map +1 -0
  7. package/dist/codemirror_language-DRHeqAwG.js +2 -0
  8. package/dist/codemirror_language-DRHeqAwG.js.map +1 -0
  9. package/dist/codemirror_state-BNbbkoNK.js +2 -0
  10. package/dist/codemirror_state-BNbbkoNK.js.map +1 -0
  11. package/dist/codemirror_view-FN7LalDk.js +2 -0
  12. package/dist/codemirror_view-FN7LalDk.js.map +1 -0
  13. package/dist/core-B4BRXuDy.js +4 -0
  14. package/dist/core-B4BRXuDy.js.map +1 -0
  15. package/dist/core.js +1 -1
  16. package/dist/{deprecations-manager-DIDxhyRq.js → deprecations-manager-BRHTwqUZ.js} +2 -2
  17. package/dist/{deprecations-manager-DIDxhyRq.js.map → deprecations-manager-BRHTwqUZ.js.map} +1 -1
  18. package/dist/{donkey-CLhmQOjG.js → donkey-CBEqGHeD.js} +2 -2
  19. package/dist/{donkey-CLhmQOjG.js.map → donkey-CBEqGHeD.js.map} +1 -1
  20. package/dist/{error-uzvvriog.js → error-DRVc1NKK.js} +2 -2
  21. package/dist/{error-uzvvriog.js.map → error-DRVc1NKK.js.map} +1 -1
  22. package/dist/index-C-U2wRvV.js +2 -0
  23. package/dist/index-C-U2wRvV.js.map +1 -0
  24. package/dist/{mpy-CnF17tqI.js → mpy-B-jI5Qug.js} +2 -2
  25. package/dist/{mpy-CnF17tqI.js.map → mpy-B-jI5Qug.js.map} +1 -1
  26. package/dist/{py-BZSSqcx3.js → py-DNLpCVR2.js} +2 -2
  27. package/dist/{py-BZSSqcx3.js.map → py-DNLpCVR2.js.map} +1 -1
  28. package/dist/{py-editor-DZ0Dxzzk.js → py-editor-DCtATRBs.js} +2 -2
  29. package/dist/{py-editor-DZ0Dxzzk.js.map → py-editor-DCtATRBs.js.map} +1 -1
  30. package/dist/{py-game-bqieV522.js → py-game-BGWt8dH1.js} +2 -2
  31. package/dist/{py-game-bqieV522.js.map → py-game-BGWt8dH1.js.map} +1 -1
  32. package/dist/{py-terminal-DYY4WN57.js → py-terminal-BKvzGq7q.js} +2 -2
  33. package/dist/{py-terminal-DYY4WN57.js.map → py-terminal-BKvzGq7q.js.map} +1 -1
  34. package/dist/xterm_addon-fit-DxKdSnof.js +14 -0
  35. package/dist/xterm_addon-fit-DxKdSnof.js.map +1 -0
  36. package/dist/xterm_addon-web-links-B6rWzrcs.js +14 -0
  37. package/dist/xterm_addon-web-links-B6rWzrcs.js.map +1 -0
  38. package/dist/zip-CgZGjqjF.js +2 -0
  39. package/dist/zip-CgZGjqjF.js.map +1 -0
  40. package/package.json +15 -14
  41. package/src/3rd-party/xterm_addon-fit.js +14 -2
  42. package/src/3rd-party/xterm_addon-web-links.js +14 -2
  43. package/src/core.js +13 -2
  44. package/src/stdlib/pyscript/__init__.py +100 -31
  45. package/src/stdlib/pyscript/context.py +198 -0
  46. package/src/stdlib/pyscript/display.py +211 -127
  47. package/src/stdlib/pyscript/events.py +191 -88
  48. package/src/stdlib/pyscript/fetch.py +156 -25
  49. package/src/stdlib/pyscript/ffi.py +132 -16
  50. package/src/stdlib/pyscript/flatted.py +78 -1
  51. package/src/stdlib/pyscript/fs.py +205 -49
  52. package/src/stdlib/pyscript/media.py +210 -50
  53. package/src/stdlib/pyscript/storage.py +214 -27
  54. package/src/stdlib/pyscript/util.py +28 -7
  55. package/src/stdlib/pyscript/web.py +1079 -881
  56. package/src/stdlib/pyscript/websocket.py +252 -45
  57. package/src/stdlib/pyscript/workers.py +176 -27
  58. package/src/stdlib/pyscript.js +13 -13
  59. package/types/stdlib/pyscript.d.ts +1 -1
  60. package/dist/codemirror-BYspKCDy.js.map +0 -1
  61. package/dist/codemirror_commands-BLDaEdQ6.js.map +0 -1
  62. package/dist/codemirror_lang-python-CkOVBHci.js +0 -2
  63. package/dist/codemirror_lang-python-CkOVBHci.js.map +0 -1
  64. package/dist/codemirror_language-DOkvasqm.js +0 -2
  65. package/dist/codemirror_language-DOkvasqm.js.map +0 -1
  66. package/dist/codemirror_state-BIAL8JKm.js +0 -2
  67. package/dist/codemirror_state-BIAL8JKm.js.map +0 -1
  68. package/dist/codemirror_view-Bt4sLgyA.js +0 -2
  69. package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
  70. package/dist/core-PTfg6inS.js +0 -4
  71. package/dist/core-PTfg6inS.js.map +0 -1
  72. package/dist/index-jZ1aOVVJ.js +0 -2
  73. package/dist/index-jZ1aOVVJ.js.map +0 -1
  74. package/dist/xterm_addon-fit--gyF3PcZ.js +0 -2
  75. package/dist/xterm_addon-fit--gyF3PcZ.js.map +0 -1
  76. package/dist/xterm_addon-web-links-D95xh2la.js +0 -2
  77. package/dist/xterm_addon-web-links-D95xh2la.js.map +0 -1
  78. package/dist/zip-pccs084i.js +0 -2
  79. package/dist/zip-pccs084i.js.map +0 -1
  80. 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 _tjs(value, **kw):
11
- if not hasattr(kw, "dict_converter"):
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 _tjs
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
- create_proxy = _cp
24
- to_js = _tjs
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
- def assign(source, *args):
46
- for arg in args:
47
- _assign(source, to_js(arg))
48
- return source
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
- # https://www.npmjs.com/package/flatted
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 get_handler(details):
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
- import js
13
- from _pyscript import fs, interpreter
14
- from pyscript.ffi import to_js
15
- from pyscript.magic_js import (
16
- RUNNING_IN_WORKER,
17
- sync,
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
- fsh = sync.storeFSHandler(uid, to_js(options))
119
+ fs_handler = sync_with_worker.storeFSHandler(mount_key, to_js(options))
33
120
 
34
- # allow both async and/or SharedArrayBuffer use case
35
- if isinstance(fsh, bool):
36
- success = fsh
121
+ # Handle both async and SharedArrayBuffer use cases.
122
+ if isinstance(fs_handler, bool):
123
+ success = fs_handler
37
124
  else:
38
- success = await fsh
125
+ success = await fs_handler
39
126
 
40
127
  if success:
41
- from polyscript import IDBMap
42
- from pyscript import window
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
- # force await in either async or sync scenario
49
- await js.Promise.resolve(sync.getFSHandler(details.options))
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(fs.ERROR)
136
+ raise RuntimeError(_fs.ERROR)
54
137
 
55
138
  else:
56
- success = await fs.idb.has(uid)
139
+ success = await _fs.idb.has(mount_key)
57
140
 
58
141
  if success:
59
- details = await fs.idb.get(uid)
60
- handler = await get_handler(details)
142
+ details = await _fs.idb.get(mount_key)
143
+ handler = await _check_permission(details)
61
144
  if handler is None:
62
- handler = await fs.getFileSystemDirectoryHandle(details.options)
145
+ handler = await _fs.getFileSystemDirectoryHandle(details.options)
63
146
  else:
64
147
  js_options = to_js(options)
65
- handler = await fs.getFileSystemDirectoryHandle(js_options)
148
+ handler = await _fs.getFileSystemDirectoryHandle(js_options)
66
149
  details = {"handler": handler, "options": js_options}
67
- await fs.idb.set(uid, to_js(details))
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 revoke(path, id="pyscript"):
73
- from _pyscript import fs, interpreter
74
- from pyscript.magic_js import (
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
- uid = f"{path}@{id}"
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
- if RUNNING_IN_WORKER:
82
- had = sync.deleteFSHandler(uid)
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
- return had
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
- async def sync(path):
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
- from _pyscript import interpreter
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