8000 Introducing pyscript.fs namespace/module (#2289) · pyscript/pyscript@0366e48 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0366e48

Browse files
Introducing pyscript.fs namespace/module (#2289)
* introducing pyscript.fs namespace/module * Added proper rejection when showDirectoryPicker is not supported * Improved exports to make explicit import in 3rd party modules easier * implemented `fs.unmount(path)`: * verified that RAM gets freed * allowed to mount different handlers within the same path through different `id` as that's the Web best way to do so
1 parent b13317d commit 0366e48

16 files changed

+383
-102
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ repos:
3434
rev: v2.4.1
3535
hooks:
3636
- id: codespell # See 'pyproject.toml' for args
37-
exclude: \.js\.map$
37+
exclude: fs\.py|\.js\.map$
3838
additional_dependencies:
3939
- tomli
4040

core/package-lock.json

Lines changed: 108 additions & 95 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pyscript/core",
3-
"version": "0.6.26",
3+
"version": "0.6.30",
44
"type": "module",
55
"description": "PyScript",
66
"module": "./index.js",
@@ -25,6 +25,10 @@
2525
"types": "./types/core.d.ts",
2626
"import": "./src/core.js"
2727
},
28+
"./js": {
29+
"types": "./types/core.d.ts",
30+
"import": "./dist/core.js"
31+
},
2832
"./css": {
2933
"import": "./dist/core.css"
3034
},
@@ -62,7 +66,7 @@
6266
"@webreflection/idb-map": "^0.3.2",
6367
"add-promise-listener": "^0.1.3",
6468
"basic-devtools": "^0.1.6",
65-
"polyscript": "^0.16.11",
69+
"polyscript": "^0.16.13",
6670
"sabayon": "^0.6.6",
6771
"sticky-module": "^0.1.1",
6872
"to-json-callback": "^0.1.1",
@@ -86,9 +90,9 @@
8690
"chokidar": "^4.0.3",
8791
"codedent": "^0.1.2",
8892
"codemirror": "^6.0.1",
89-
"eslint": "^9.19.0",
93+
"eslint": "^9.20.1",
9094
"flatted": "^3.3.2",
91-
"rollup": "^4.34.4",
95+
"rollup": "^4.34.7",
9296
"rollup-plugin-postcss": "^4.0.2",
9397
"rollup-plugin-string": "^3.0.0",
9498
"static-handler": "^0.5.3",

core/src/core.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,13 @@ py-terminal span,
7878
mpy-terminal span {
7979
letter-spacing: 0 !important;
8080
}
81+
82+
dialog.pyscript-fs {
83+
border-radius: 8px;
84+
border-width: 1px;
85+
}
86+
87+
dialog.pyscript-fs > div {
88+
display: flex;
89+
justify F438 -content: space-between;
90+
}

core/src/core.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
createFunction,
3434
inputFailure,
3535
} from "./hooks.js";
36+
import * as fs from "./fs.js";
3637

3738
import codemirror from "./plugins/codemirror.js";
3839
export { codemirror };
@@ -167,6 +168,8 @@ for (const [TYPE, interpreter] of TYPES) {
167168
// enrich the Python env with some JS utility for main
168169
interpreter.registerJsModule("_pyscript", {
169170
PyWorker,
171+
fs,
172+
interpreter,
170173
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
171174
get target() {
172175
return isScript(currentElement)

core/src/fs.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import IDBMap from "@webreflection/idb-map";
2+
import { assign } from "polyscript/exports";
3+
import { $$ } from "basic-devtools";
4+
5+
const stop = (event) => {
6+
event.preventDefault();
7+
event.stopImmediatePropagation();
8+
};
9+
10+
// ⚠️ these two constants MUST be passed as `fs`
11+
// within the worker onBeforeRunAsync hook!
12+
export const NAMESPACE = "@pyscript.fs";
13+
export const ERROR = "storage permissions not granted";
14+
15+
export const idb< 10000 /span> = new IDBMap(NAMESPACE);
16+
17+
/**
18+
* Ask a user action via dialog and returns the directory handler once granted.
19+
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
20+
* @returns {Promise<FileSystemDirectoryHandle>}
21+
*/
22+
export const getFileSystemDirectoryHandle = async (options) => {
23+
if (!("showDirectoryPicker" in globalThis)) {
24+
return Promise.reject(
25+
new Error("showDirectoryPicker is not supported"),
26+
);
27+
}
28+
29+
const { promise, resolve, reject } = Promise.withResolvers();
30+
31+
const how = { id: "pyscript", mode: "readwrite", ...options };
32+
if (options.hint) how.startIn = options.hint;
33+
34+
const transient = async () => {
35+
try {
36+
/* eslint-disable */
37+
const handler = await showDirectoryPicker(how);
38+
/* eslint-enable */
39+
if ((await handler.requestPermission(how)) === "granted") {
40+
resolve(handler);
41+
return true;
42+
}
43+
} catch ({ message }) {
44+
console.warn(message);
45+
}
46+
return false;
47+
};
48+
49+
// in case the user decided to attach the event itself
50+
// as opposite of relying our dialog walkthrough
51+
if (navigator.userActivation?.isActive) {
52+
if (!(await transient())) reject(new Error(ERROR));
53+
} else {
54+
const dialog = assign(document.createElement("dialog"), {
55+
className: "pyscript-fs",
56+
innerHTML: [
57+
"<strong>ℹ️ Persistent FileSystem</strong><hr>",
58+
"<p><small>PyScript would like to access a local folder.</small></p>",
59+
"<div><button title='ok'>✅ Authorize</button>",
60+
"<button title='cancel'>❌</button></div>",
61+
].join(""),
62+
});
63+
64+
const [ok, cancel] = $$("button", dialog);
65+
66+
ok.addEventListener("click", async (event) => {
67+
stop(event);
68+
if (await transient()) dialog.close();
69+
});
70+
71+
cancel.addEventListener("click", async (event) => {
72+
stop(event);
73+
reject(new Error(ERROR));
74+
dialog.close();
75+
});
76+
77+
document.body.appendChild(dialog).showModal();
78+
}
79+
80+
return promise;
81+
};

core/src/hooks.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,19 @@ export const hooks = {
8888
/** @type {Set<function>} */
8989
onBeforeRun: new SetFunction(),
9090
/** @type {Set<function>} */
91-
onBeforeRunAsync: new SetFunction(),
91+
onBeforeRunAsync: new SetFunction([
92+
({ interpreter }) => {
93+
interpreter.registerJsModule("_pyscript", {
94+
// cannot be imported from fs.js
95+
// because this code is stringified
96+
fs: {
97+
ERROR: "storage permissions not granted",
98+
NAMESPACE: "@pyscript.fs",
99+
},
100+
interpreter,
101+
});
102+
},
103+
]),
92104
/** @type {Set<function>} */
93105
onAfterRun: new SetFunction(),
94106
/** @type {Set<function>} */

core/src/stdlib/pyscript.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/stdlib/pyscript/fs.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
mounted = {}
2+
3+
4+
async def mount(path, mode="readwrite", root="", id="pyscript"):
5+
import js
6+
from _pyscript import fs, interpreter
7+
from pyscript.ffi import to_js
8+
from pyscript.magic_js import (
9+
RUNNING_IN_WORKER,
10+
sync,
11+
)
12+
13+
js.console.warn("experimental pyscript.fs ⚠️")
14+
15+
handler = None
16+
17+
uid = f"{path}@{id}"
18+
19+
options = {"id": id, "mode": mode}
20+
if root != "":
21+
options["startIn"] = root
22+
23+
if RUNNING_IN_WORKER:
24+
fsh = sync.storeFSHandler(uid, to_js(options))
25+
26+
# allow both async and/or SharedArrayBuffer use case
27+
if isinstance(fsh, bool):
28+
success = fsh
29+
else:
30+
success = await fsh
31+
32+
if success:
33+
from polyscript import IDBMap
34+
35+
idb = IDBMap.new(fs.NAMESPACE)
36+
handler = await idb.get(uid)
37+
else:
38+
raise RuntimeError(fs.ERROR)
39+
40+
else:
41+
success = await fs.idb.has(uid)
42+
43+
if success:
44+
handler = await fs.idb.get(uid)
45+
else:
46+
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
47+
await fs.idb.set(uid, handler)
48+
49+
mounted[path] = await interpreter.mountNativeFS(path, handler)
50+
51+
52+
async def sync(path):
53+
await mounted[path].syncfs()
54+
55+
56+
async def unmount(path):
57+
from _pyscript import interpreter
58+
59+
await sync(path)
60+
interpreter._module.FS.unmount(path)

core/src/sync.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { idb, getFileSystemDirectoryHandle } from "./fs.js";
2+
13
export default {
24
// allow pyterminal checks to bootstrap
35
is_pyterminal: () => false,
@@ -9,4 +11,21 @@ export default {
911
sleep(seconds) {
1012
return new Promise(($) => setTimeout($, seconds * 1000));
1113
},
14+
15+
/**
16+
* Ask a user action via dialog and returns the directory handler once granted.
17+
* @param {string} uid
18+
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
19+
* @returns {boolean}
20+
*/
21+
async storeFSHandler(uid, options = {}) {
22+
if (await idb.has(uid)) return true;
23+
return getFileSystemDirectoryHandle(options).then(
24+
async (handler) => {
25+
await idb.set(uid, handler);
26+
return true;
27+
},
28+
() => false,
29+
);
30+
},
1231
};

0 commit comments

Comments
 (0)
0