@pyscript/core 0.1.18 → 0.1.20
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/README.md +19 -2
- package/dist/core.js +3 -3
- package/dist/core.js.map +1 -1
- package/dist/error-87e0706c.js +2 -0
- package/dist/error-87e0706c.js.map +1 -0
- package/docs/README.md +3 -12
- package/package.json +4 -3
- package/src/config.js +110 -0
- package/src/core.js +84 -106
- package/src/fetch.js +3 -0
- package/src/plugins/error.js +1 -1
- package/src/plugins.js +1 -1
- package/src/stdlib/pyscript/__init__.py +34 -0
- package/src/stdlib/pyscript/display.py +154 -0
- package/src/stdlib/pyscript/event_handling.py +45 -0
- package/src/stdlib/pyscript/magic_js.py +32 -0
- package/src/stdlib/pyscript/util.py +22 -0
- package/src/stdlib/pyscript.js +9 -5
- package/src/stdlib/pyweb/pydom.py +314 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +184 -0
- package/tests/integration/support.py +1038 -0
- package/tests/integration/test_00_support.py +495 -0
- package/tests/integration/test_01_basic.py +353 -0
- package/tests/integration/test_02_display.py +452 -0
- package/tests/integration/test_03_element.py +303 -0
- package/tests/integration/test_assets/line_plot.png +0 -0
- package/tests/integration/test_assets/tripcolor.png +0 -0
- package/tests/integration/test_async.py +197 -0
- package/tests/integration/test_event_handling.py +193 -0
- package/tests/integration/test_importmap.py +66 -0
- package/tests/integration/test_interpreter.py +98 -0
- package/tests/integration/test_plugins.py +419 -0
- package/tests/integration/test_py_config.py +294 -0
- package/tests/integration/test_py_repl.py +663 -0
- package/tests/integration/test_py_terminal.py +270 -0
- package/tests/integration/test_runtime_attributes.py +64 -0
- package/tests/integration/test_script_type.py +121 -0
- package/tests/integration/test_shadow_root.py +33 -0
- package/tests/integration/test_splashscreen.py +124 -0
- package/tests/integration/test_stdio_handling.py +370 -0
- package/tests/integration/test_style.py +47 -0
- package/tests/integration/test_warnings_and_banners.py +32 -0
- package/tests/integration/test_zz_examples.py +419 -0
- package/tests/integration/test_zzz_docs_snippets.py +305 -0
- package/types/config.d.ts +3 -0
- package/types/core.d.ts +1 -2
- package/types/fetch.d.ts +1 -0
- package/types/plugins/error.d.ts +1 -1
- package/types/stdlib/pyscript.d.ts +8 -4
- package/dist/error-91f1c2f6.js +0 -2
- package/dist/error-91f1c2f6.js.map +0 -1
@@ -0,0 +1,2 @@
|
|
1
|
+
import{hooks as e}from"./core.js";function o(e){const o=document.createElement("div");o.className="py-error",o.textContent=e,o.style.cssText="\n border: 1px solid red;\n background: #ffdddd;\n color: black;\n font-family: courier, monospace;\n white-space: pre;\n overflow-x: auto;\n padding: 8px;\n margin-top: 8px;\n ",document.body.append(o)}e.onBeforeRun.add((function n(r){e.onBeforeRun.delete(n);const{stderr:t}=r.io;r.io.stderr=(e,...n)=>(o(e.message||e),t(e,...n)),addEventListener("error",(({message:e})=>{e.startsWith("Uncaught PythonError")&&o(e)}))}));export{o as notify};
|
2
|
+
//# sourceMappingURL=error-87e0706c.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"error-87e0706c.js","sources":["../src/plugins/error.js"],"sourcesContent":["// PyScript Error Plugin\nimport { hooks } from \"../core.js\";\n\nhooks.onBeforeRun.add(function override(pyScript) {\n // be sure this override happens only once\n hooks.onBeforeRun.delete(override);\n\n // trap generic `stderr` to propagate to it regardless\n const { stderr } = pyScript.io;\n\n // override it with our own logic\n pyScript.io.stderr = (error, ...rest) => {\n notify(error.message || error);\n // let other plugins or stderr hook, if any, do the rest\n return stderr(error, ...rest);\n };\n\n // be sure uncaught Python errors are also visible\n addEventListener(\"error\", ({ message }) => {\n if (message.startsWith(\"Uncaught PythonError\")) notify(message);\n });\n});\n\n// Error hook utilities\n\n// Custom function to show notifications\nexport function notify(message) {\n const div = document.createElement(\"div\");\n div.className = \"py-error\";\n div.textContent = message;\n div.style.cssText = `\n border: 1px solid red;\n background: #ffdddd;\n color: black;\n font-family: courier, monospace;\n white-space: pre;\n overflow-x: auto;\n padding: 8px;\n margin-top: 8px;\n `;\n document.body.append(div);\n}\n"],"names":["notify","message","div","document","createElement","className","textContent","style","cssText","body","append","hooks","onBeforeRun","add","override","pyScript","delete","stderr","io","error","rest","addEventListener","startsWith"],"mappings":"kCA0BO,SAASA,EAAOC,GACnB,MAAMC,EAAMC,SAASC,cAAc,OACnCF,EAAIG,UAAY,WAChBH,EAAII,YAAcL,EAClBC,EAAIK,MAAMC,QAAU,6MAUpBL,SAASM,KAAKC,OAAOR,EACzB,CAtCAS,EAAMC,YAAYC,KAAI,SAASC,EAASC,GAEpCJ,EAAMC,YAAYI,OAAOF,GAGzB,MAAMG,OAAEA,GAAWF,EAASG,GAG5BH,EAASG,GAAGD,OAAS,CAACE,KAAUC,KAC5BpB,EAAOmB,EAAMlB,SAAWkB,GAEjBF,EAAOE,KAAUC,IAI5BC,iBAAiB,SAAS,EAAGpB,cACrBA,EAAQqB,WAAW,yBAAyBtB,EAAOC,EAAQ,GAEvE"}
|
package/docs/README.md
CHANGED
@@ -111,7 +111,7 @@ import { hooks } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
|
|
111
111
|
|
112
112
|
// example
|
113
113
|
hooks.onInterpreterReady.add((utils, element) => {
|
114
|
-
console.
|
114
|
+
console.log(element, 'found', 'pyscript is ready');
|
115
115
|
});
|
116
116
|
|
117
117
|
// the hooks namespace
|
@@ -148,9 +148,9 @@ Please note that a *worker* is a completely different environment and it's not p
|
|
148
148
|
|
149
149
|
However, each worker string can use `from pyscript import x, y, z` as that will be available out of the box.
|
150
150
|
|
151
|
-
## PyScript
|
151
|
+
## PyScript Python API
|
152
152
|
|
153
|
-
The python
|
153
|
+
The `pyscript` python package offers various utilities in either the main thread or the worker.
|
154
154
|
|
155
155
|
The commonly shared utilities are:
|
156
156
|
|
@@ -257,15 +257,6 @@ We might decide to allow TOML too in the future, but the direct config as attrib
|
|
257
257
|
</div>
|
258
258
|
</details>
|
259
259
|
|
260
|
-
<details>
|
261
|
-
<summary><strong>why worker attribute needs an external file?</strong></summary>
|
262
|
-
<div markdown=1>
|
263
|
-
|
264
|
-
It would create confusion to have worker code embedded directly in the page and let *PyScript* forward the content to be executed as worker, but the separation of concerns felt more aligned with the meaning of bootstrapping a worker: it inevitably happens elsewhere and with little caveats or features here and there, so it's OK for now to keep that separation explicit.
|
265
|
-
|
266
|
-
</div>
|
267
|
-
</details>
|
268
|
-
|
269
260
|
<details>
|
270
261
|
<summary><strong>what are the worker's caveats?</strong></summary>
|
271
262
|
<div markdown=1>
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@pyscript/core",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.20",
|
4
4
|
"type": "module",
|
5
5
|
"description": "PyScript",
|
6
6
|
"module": "./index.js",
|
@@ -21,6 +21,7 @@
|
|
21
21
|
"scripts": {
|
22
22
|
"server": "npx static-handler --cors --coep --coop --corp .",
|
23
23
|
"build": "node rollup/stdlib.cjs && node rollup/plugins.cjs && rm -rf dist && rollup --config rollup/core.config.js && npm run ts",
|
24
|
+
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do echo -e \"\\033[2m$js:\\033[0m $(cat $js | brotli | wc -c) bytes\"; done",
|
24
25
|
"ts": "tsc -p ."
|
25
26
|
},
|
26
27
|
"keywords": [
|
@@ -32,12 +33,12 @@
|
|
32
33
|
"dependencies": {
|
33
34
|
"@ungap/with-resolvers": "^0.1.0",
|
34
35
|
"basic-devtools": "^0.1.6",
|
35
|
-
"polyscript": "^0.3.
|
36
|
+
"polyscript": "^0.3.7"
|
36
37
|
},
|
37
38
|
"devDependencies": {
|
38
39
|
"@rollup/plugin-node-resolve": "^15.2.1",
|
39
40
|
"@rollup/plugin-terser": "^0.4.3",
|
40
|
-
"rollup": "^3.29.
|
41
|
+
"rollup": "^3.29.2",
|
41
42
|
"rollup-plugin-postcss": "^4.0.2",
|
42
43
|
"rollup-plugin-string": "^3.0.0",
|
43
44
|
"static-handler": "^0.4.2",
|
package/src/config.js
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
/**
|
2
|
+
* This file parses a generic <py-config> or config attribute
|
3
|
+
* to use as base config for all py-script elements, importing
|
4
|
+
* also a queue of plugins *before* the interpreter (if any) resolves.
|
5
|
+
*/
|
6
|
+
import { $ } from "basic-devtools";
|
7
|
+
|
8
|
+
import allPlugins from "./plugins.js";
|
9
|
+
import { robustFetch as fetch, getText } from "./fetch.js";
|
10
|
+
import { ErrorCode } from "./exceptions.js";
|
11
|
+
|
12
|
+
const badURL = (url, expected = "") => {
|
13
|
+
let message = `(${ErrorCode.BAD_CONFIG}): Invalid URL: ${url}`;
|
14
|
+
if (expected) message += `\nexpected ${expected} content`;
|
15
|
+
throw new Error(message);
|
16
|
+
};
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Given a string, returns its trimmed content as text,
|
20
|
+
* fetching it from a file if the content is a URL.
|
21
|
+
* @param {string} config either JSON, TOML, or a file to fetch
|
22
|
+
* @returns {{json: boolean, toml: boolean, text: string}}
|
23
|
+
*/
|
24
|
+
const configDetails = async (config) => {
|
25
|
+
let text = config?.trim();
|
26
|
+
// we only support an object as root config
|
27
|
+
let url = "",
|
28
|
+
toml = false,
|
29
|
+
json = /^{/.test(text) && /}$/.test(text);
|
30
|
+
// handle files by extension (relaxing urls parts after)
|
31
|
+
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
|
32
|
+
const ext = RegExp.$1;
|
33
|
+
if (ext === "json" && type !== "toml") json = true;
|
34
|
+
else if (ext === "toml" && type !== "json") toml = true;
|
35
|
+
else badURL(text, type);
|
36
|
+
url = text;
|
37
|
+
text = (await fetch(url).then(getText)).trim();
|
38
|
+
}
|
39
|
+
return { json, toml: toml || (!json && !!text), text, url };
|
40
|
+
};
|
41
|
+
|
42
|
+
const syntaxError = (type, url, { message }) => {
|
43
|
+
let str = `(${ErrorCode.BAD_CONFIG}): Invalid ${type}`;
|
44
|
+
if (url) str += ` @ ${url}`;
|
45
|
+
return new SyntaxError(`${str}\n${message}`);
|
46
|
+
};
|
47
|
+
|
48
|
+
// find the shared config for all py-script elements
|
49
|
+
let config, plugins, parsed, error, type;
|
50
|
+
let pyConfig = $("py-config");
|
51
|
+
if (pyConfig) {
|
52
|
+
config = pyConfig.getAttribute("src") || pyConfig.textContent;
|
53
|
+
type = pyConfig.getAttribute("type");
|
54
|
+
} else {
|
55
|
+
pyConfig = $(
|
56
|
+
[
|
57
|
+
'script[type="py"][config]:not([worker])',
|
58
|
+
"py-script[config]:not([worker])",
|
59
|
+
].join(","),
|
60
|
+
);
|
61
|
+
if (pyConfig) config = pyConfig.getAttribute("config");
|
62
|
+
}
|
63
|
+
|
64
|
+
// catch possible fetch errors
|
65
|
+
if (config) {
|
66
|
+
try {
|
67
|
+
const { json, toml, text, url } = await configDetails(config);
|
68
|
+
config = text;
|
69
|
+
if (json || type === "json") {
|
70
|
+
try {
|
71
|
+
parsed = JSON.parse(text);
|
72
|
+
} catch (e) {
|
73
|
+
error = syntaxError("JSON", url, e);
|
74
|
+
}
|
75
|
+
} else if (toml || type === "toml") {
|
76
|
+
try {
|
77
|
+
const { parse } = await import(
|
78
|
+
/* webpackIgnore: true */
|
79
|
+
"https://cdn.jsdelivr.net/npm/@webreflection/toml-j0.4/toml.js"
|
80
|
+
);
|
81
|
+
parsed = parse(text);
|
82
|
+
} catch (e) {
|
83
|
+
error = syntaxError("TOML", url, e);
|
84
|
+
}
|
85
|
+
}
|
86
|
+
} catch (e) {
|
87
|
+
error = e;
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
// parse all plugins and optionally ignore only
|
92
|
+
// those flagged as "undesired" via `!` prefix
|
93
|
+
const toBeAwaited = [];
|
94
|
+
for (const [key, value] of Object.entries(allPlugins)) {
|
95
|
+
if (error) {
|
96
|
+
if (key === "error") {
|
97
|
+
// show on page the config is broken, meaning that
|
98
|
+
// it was not possible to disable error plugin neither
|
99
|
+
// as that part wasn't correctly parsed anyway
|
100
|
+
value().then(({ notify }) => notify(error.message));
|
101
|
+
}
|
102
|
+
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
103
|
+
toBeAwaited.push(value());
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
// assign plugins as Promise.all only if needed
|
108
|
+
if (toBeAwaited.length) plugins = Promise.all(toBeAwaited);
|
109
|
+
|
110
|
+
export { config, plugins, error };
|
package/src/core.js
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
/*! (c) PyScript Development Team */
|
2
2
|
|
3
3
|
import "@ungap/with-resolvers";
|
4
|
-
import { $ } from "basic-devtools";
|
5
4
|
import { define, XWorker } from "polyscript";
|
6
|
-
import sync from "./sync.js";
|
7
|
-
|
8
|
-
import stdlib from "./stdlib.js";
|
9
|
-
import plugins from "./plugins.js";
|
10
5
|
|
11
6
|
// TODO: this is not strictly polyscript related but handy ... not sure
|
12
7
|
// we should factor this utility out a part but this works anyway.
|
@@ -14,29 +9,21 @@ import { queryTarget } from "../node_modules/polyscript/esm/script-handler.js";
|
|
14
9
|
import { dedent, dispatch } from "../node_modules/polyscript/esm/utils.js";
|
15
10
|
import { Hook } from "../node_modules/polyscript/esm/worker/hooks.js";
|
16
11
|
|
17
|
-
import
|
12
|
+
import sync from "./sync.js";
|
13
|
+
import stdlib from "./stdlib.js";
|
14
|
+
import { config, plugins, error } from "./config.js";
|
15
|
+
import { robustFetch as fetch, getText } from "./fetch.js";
|
18
16
|
|
19
17
|
const { assign, defineProperty, entries } = Object;
|
20
18
|
|
21
|
-
const
|
19
|
+
const TYPE = "py";
|
22
20
|
|
23
21
|
// allows lazy element features on code evaluation
|
24
22
|
let currentElement;
|
25
23
|
|
26
24
|
// create a unique identifier when/if needed
|
27
25
|
let id = 0;
|
28
|
-
const getID = (prefix =
|
29
|
-
|
30
|
-
// find the shared config for all py-script elements
|
31
|
-
let config;
|
32
|
-
let pyConfig = $("py-config");
|
33
|
-
if (pyConfig) config = pyConfig.getAttribute("src") || pyConfig.textContent;
|
34
|
-
else {
|
35
|
-
pyConfig = $('script[type="py"]');
|
36
|
-
config = pyConfig?.getAttribute("config");
|
37
|
-
}
|
38
|
-
|
39
|
-
if (/^https?:\/\//.test(config)) config = await fetch(config).then(getText);
|
26
|
+
const getID = (prefix = TYPE) => `${prefix}-${id++}`;
|
40
27
|
|
41
28
|
// generic helper to disambiguate between custom element and script
|
42
29
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
@@ -70,7 +57,7 @@ const fetchSource = async (tag, io, asText) => {
|
|
70
57
|
if (asText) return dedent(tag.textContent);
|
71
58
|
|
72
59
|
console.warn(
|
73
|
-
|
60
|
+
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
|
74
61
|
tag.innerHTML,
|
75
62
|
);
|
76
63
|
|
@@ -95,7 +82,7 @@ const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
|
95
82
|
}
|
96
83
|
|
97
84
|
// enrich the Python env with some JS utility for main
|
98
|
-
interpreter.registerJsModule("
|
85
|
+
interpreter.registerJsModule("_pyscript", {
|
99
86
|
PyWorker,
|
100
87
|
get target() {
|
101
88
|
return isScript(currentElement)
|
@@ -140,92 +127,82 @@ const workerHooks = {
|
|
140
127
|
[...hooks.codeAfterRunWorkerAsync].map(dedent).join("\n"),
|
141
128
|
};
|
142
129
|
|
143
|
-
// avoid running further script if the previous one had
|
144
|
-
// some import that would inevitably delay its execution
|
145
|
-
let queuePlugins;
|
146
|
-
|
147
130
|
// define the module as both `<script type="py">` and `<py-script>`
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
}
|
182
|
-
|
183
|
-
// this grants queued results when first script/tag has plugins
|
184
|
-
// and the second one *might* rely on first tag execution
|
185
|
-
if (toBeAwaited.length) {
|
186
|
-
const all = Promise.all(toBeAwaited);
|
187
|
-
queuePlugins = queuePlugins ? queuePlugins.then(() => all) : all;
|
188
|
-
}
|
189
|
-
|
190
|
-
if (queuePlugins) await queuePlugins;
|
191
|
-
|
192
|
-
// allows plugins to do whatever they want with the element
|
193
|
-
// before regular stuff happens in here
|
194
|
-
for (const callback of hooks.onInterpreterReady)
|
195
|
-
callback(pyodide, element);
|
196
|
-
|
197
|
-
if (isScript(element)) {
|
198
|
-
const {
|
199
|
-
attributes: { async: isAsync, target },
|
200
|
-
} = element;
|
201
|
-
const hasTarget = !!target?.value;
|
202
|
-
const show = hasTarget
|
203
|
-
? queryTarget(target.value)
|
204
|
-
: document.createElement("script-py");
|
205
|
-
|
206
|
-
if (!hasTarget) {
|
207
|
-
const { head, body } = document;
|
208
|
-
if (head.contains(element)) body.append(show);
|
209
|
-
else element.after(show);
|
131
|
+
// but only if the config didn't throw an error
|
132
|
+
error ||
|
133
|
+
define(TYPE, {
|
134
|
+
config,
|
135
|
+
env: `${TYPE}-script`,
|
136
|
+
interpreter: "pyodide",
|
137
|
+
...workerHooks,
|
138
|
+
onWorkerReady(_, xworker) {
|
139
|
+
assign(xworker.sync, sync);
|
140
|
+
},
|
141
|
+
onBeforeRun(pyodide, element) {
|
142
|
+
currentElement = element;
|
143
|
+
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun");
|
144
|
+
},
|
145
|
+
onBeforeRunAsync(pyodide, element) {
|
146
|
+
currentElement = element;
|
147
|
+
bootstrapNodeAndPlugins(
|
148
|
+
pyodide,
|
149
|
+
element,
|
150
|
+
before,
|
151
|
+
"onBeforeRunAsync",
|
152
|
+
);
|
153
|
+
},
|
154
|
+
onAfterRun(pyodide, element) {
|
155
|
+
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun");
|
156
|
+
},
|
157
|
+
onAfterRunAsync(pyodide, element) {
|
158
|
+
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
159
|
+
},
|
160
|
+
async onInterpreterReady(pyodide, element) {
|
161
|
+
if (shouldRegister) {
|
162
|
+
shouldRegister = false;
|
163
|
+
registerModule(pyodide);
|
210
164
|
}
|
211
|
-
if (!show.id) show.id = getID();
|
212
165
|
|
213
|
-
//
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
//
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
)
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
166
|
+
// ensure plugins are bootstrapped already
|
167
|
+
if (plugins) await plugins;
|
168
|
+
|
169
|
+
// allows plugins to do whatever they want with the element
|
170
|
+
// before regular stuff happens in here
|
171
|
+
for (const callback of hooks.onInterpreterReady)
|
172
|
+
callback(pyodide, element);
|
173
|
+
|
174
|
+
if (isScript(element)) {
|
175
|
+
const {
|
176
|
+
attributes: { async: isAsync, target },
|
177
|
+
} = element;
|
178
|
+
const hasTarget = !!target?.value;
|
179
|
+
const show = hasTarget
|
180
|
+
? queryTarget(target.value)
|
181
|
+
: document.createElement("script-py");
|
182
|
+
|
183
|
+
if (!hasTarget) {
|
184
|
+
const { head, body } = document;
|
185
|
+
if (head.contains(element)) body.append(show);
|
186
|
+
else element.after(show);
|
187
|
+
}
|
188
|
+
if (!show.id) show.id = getID();
|
189
|
+
|
190
|
+
// allows the code to retrieve the target element via
|
191
|
+
// document.currentScript.target if needed
|
192
|
+
defineProperty(element, "target", { value: show });
|
193
|
+
|
194
|
+
// notify before the code runs
|
195
|
+
dispatch(element, TYPE);
|
196
|
+
pyodide[`run${isAsync ? "Async" : ""}`](
|
197
|
+
await fetchSource(element, pyodide.io, true),
|
198
|
+
);
|
199
|
+
} else {
|
200
|
+
// resolve PyScriptElement to allow connectedCallback
|
201
|
+
element._pyodide.resolve(pyodide);
|
202
|
+
}
|
203
|
+
console.debug("[pyscript/main] PyScript Ready");
|
204
|
+
},
|
205
|
+
});
|
229
206
|
|
230
207
|
class PyScriptElement extends HTMLElement {
|
231
208
|
constructor() {
|
@@ -249,14 +226,15 @@ class PyScriptElement extends HTMLElement {
|
|
249
226
|
this.srcCode = await fetchSource(this, io, !this.childElementCount);
|
250
227
|
this.replaceChildren();
|
251
228
|
// notify before the code runs
|
252
|
-
dispatch(this,
|
229
|
+
dispatch(this, TYPE);
|
253
230
|
runner(this.srcCode);
|
254
231
|
this.style.display = "block";
|
255
232
|
}
|
256
233
|
}
|
257
234
|
}
|
258
235
|
|
259
|
-
|
236
|
+
// define py-script only if the config didn't throw an error
|
237
|
+
error || customElements.define("py-script", PyScriptElement);
|
260
238
|
|
261
239
|
/**
|
262
240
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
package/src/fetch.js
CHANGED
package/src/plugins/error.js
CHANGED
@@ -24,7 +24,7 @@ hooks.onBeforeRun.add(function override(pyScript) {
|
|
24
24
|
// Error hook utilities
|
25
25
|
|
26
26
|
// Custom function to show notifications
|
27
|
-
function notify(message) {
|
27
|
+
export function notify(message) {
|
28
28
|
const div = document.createElement("div");
|
29
29
|
div.className = "py-error";
|
30
30
|
div.textContent = message;
|
package/src/plugins.js
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Some notes about the naming conventions and the relationship between various
|
2
|
+
# similar-but-different names.
|
3
|
+
#
|
4
|
+
# import pyscript
|
5
|
+
# this package contains the main user-facing API offered by pyscript. All
|
6
|
+
# the names which are supposed be used by end users should be made
|
7
|
+
# available in pyscript/__init__.py (i.e., this file)
|
8
|
+
#
|
9
|
+
# import _pyscript
|
10
|
+
# this is an internal module implemented in JS. It is used internally by
|
11
|
+
# the pyscript package, end users should not use it directly. For its
|
12
|
+
# implementation, grep for `interpreter.registerJsModule("_pyscript",
|
13
|
+
# ...)` in core.js
|
14
|
+
#
|
15
|
+
# import js
|
16
|
+
# this is the JS globalThis, as exported by pyodide and/or micropython's
|
17
|
+
# FFIs. As such, it contains different things in the main thread or in a
|
18
|
+
# worker.
|
19
|
+
#
|
20
|
+
# import pyscript.magic_js
|
21
|
+
# this submodule abstracts away some of the differences between the main
|
22
|
+
# thread and the worker. In particular, it defines `window` and `document`
|
23
|
+
# in such a way that these names work in both cases: in the main thread,
|
24
|
+
# they are the "real" objects, in the worker they are proxies which work
|
25
|
+
# thanks to coincident.
|
26
|
+
#
|
27
|
+
# from pyscript import window, document
|
28
|
+
# these are just the window and document objects as defined by
|
29
|
+
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
30
|
+
# as it works transparently in both the main thread and worker cases.
|
31
|
+
|
32
|
+
from pyscript.magic_js import RUNNING_IN_WORKER, window, document, sync
|
33
|
+
from pyscript.display import HTML, display
|
34
|
+
from pyscript.event_handling import when
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import base64
|
2
|
+
import html
|
3
|
+
import io
|
4
|
+
import re
|
5
|
+
|
6
|
+
from pyscript.magic_js import document, window, current_target
|
7
|
+
|
8
|
+
_MIME_METHODS = {
|
9
|
+
"__repr__": "text/plain",
|
10
|
+
"_repr_html_": "text/html",
|
11
|
+
"_repr_markdown_": "text/markdown",
|
12
|
+
"_repr_svg_": "image/svg+xml",
|
13
|
+
"_repr_png_": "image/png",
|
14
|
+
"_repr_pdf_": "application/pdf",
|
15
|
+
"_repr_jpeg_": "image/jpeg",
|
16
|
+
"_repr_latex": "text/latex",
|
17
|
+
"_repr_json_": "application/json",
|
18
|
+
"_repr_javascript_": "application/javascript",
|
19
|
+
"savefig": "image/png",
|
20
|
+
}
|
21
|
+
|
22
|
+
|
23
|
+
def _render_image(mime, value, meta):
|
24
|
+
# If the image value is using bytes we should convert it to base64
|
25
|
+
# otherwise it will return raw bytes and the browser will not be able to
|
26
|
+
# render it.
|
27
|
+
if isinstance(value, bytes):
|
28
|
+
value = base64.b64encode(value).decode("utf-8")
|
29
|
+
|
30
|
+
# This is the pattern of base64 strings
|
31
|
+
base64_pattern = re.compile(
|
32
|
+
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
33
|
+
)
|
34
|
+
# If value doesn't match the base64 pattern we should encode it to base64
|
35
|
+
if len(value) > 0 and not base64_pattern.match(value):
|
36
|
+
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
37
|
+
|
38
|
+
data = f"data:{mime};charset=utf-8;base64,{value}"
|
39
|
+
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
40
|
+
return f'<img src="{data}" {attrs}></img>'
|
41
|
+
|
42
|
+
|
43
|
+
def _identity(value, meta):
|
44
|
+
return value
|
45
|
+
|
46
|
+
|
47
|
+
_MIME_RENDERERS = {
|
48
|
+
"text/plain": html.escape,
|
49
|
+
"text/html": _identity,
|
50
|
+
"image/png": lambda value, meta: _render_image("image/png", value, meta),
|
51
|
+
"image/jpeg": lambda value, meta: _render_image("image/jpeg", value, meta),
|
52
|
+
"image/svg+xml": _identity,
|
53
|
+
"application/json": _identity,
|
54
|
+
"application/javascript": lambda value, meta: f"<script>{value}<\\/script>",
|
55
|
+
}
|
56
|
+
|
57
|
+
|
58
|
+
class HTML:
|
59
|
+
"""
|
60
|
+
Wrap a string so that display() can render it as plain HTML
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(self, html):
|
64
|
+
self._html = html
|
65
|
+
|
66
|
+
def _repr_html_(self):
|
67
|
+
return self._html
|
68
|
+
|
69
|
+
|
70
|
+
def _eval_formatter(obj, print_method):
|
71
|
+
"""
|
72
|
+
Evaluates a formatter method.
|
73
|
+
"""
|
74
|
+
if print_method == "__repr__":
|
75
|
+
return repr(obj)
|
76
|
+
elif hasattr(obj, print_method):
|
77
|
+
if print_method == "savefig":
|
78
|
+
buf = io.BytesIO()
|
79
|
+
obj.savefig(buf, format="png")
|
80
|
+
buf.seek(0)
|
81
|
+
return base64.b64encode(buf.read()).decode("utf-8")
|
82
|
+
return getattr(obj, print_method)()
|
83
|
+
elif print_method == "_repr_mimebundle_":
|
84
|
+
return {}, {}
|
85
|
+
return None
|
86
|
+
|
87
|
+
|
88
|
+
def _format_mime(obj):
|
89
|
+
"""
|
90
|
+
Formats object using _repr_x_ methods.
|
91
|
+
"""
|
92
|
+
if isinstance(obj, str):
|
93
|
+
return html.escape(obj), "text/plain"
|
94
|
+
|
95
|
+
mimebundle = _eval_formatter(obj, "_repr_mimebundle_")
|
96
|
+
if isinstance(mimebundle, tuple):
|
97
|
+
format_dict, _ = mimebundle
|
98
|
+
else:
|
99
|
+
format_dict = mimebundle
|
100
|
+
|
101
|
+
output, not_available = None, []
|
102
|
+
for method, mime_type in reversed(_MIME_METHODS.items()):
|
103
|
+
if mime_type in format_dict:
|
104
|
+
output = format_dict[mime_type]
|
105
|
+
else:
|
106
|
+
output = _eval_formatter(obj, method)
|
107
|
+
|
108
|
+
if output is None:
|
109
|
+
continue
|
110
|
+
elif mime_type not in _MIME_RENDERERS:
|
111
|
+
not_available.append(mime_type)
|
112
|
+
continue
|
113
|
+
break
|
114
|
+
if output is None:
|
115
|
+
if not_available:
|
116
|
+
window.console.warn(
|
117
|
+
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
118
|
+
)
|
119
|
+
output = repr(output)
|
120
|
+
mime_type = "text/plain"
|
121
|
+
elif isinstance(output, tuple):
|
122
|
+
output, meta = output
|
123
|
+
else:
|
124
|
+
meta = {}
|
125
|
+
return _MIME_RENDERERS[mime_type](output, meta), mime_type
|
126
|
+
|
127
|
+
|
128
|
+
def _write(element, value, append=False):
|
129
|
+
html, mime_type = _format_mime(value)
|
130
|
+
if html == "\\n":
|
131
|
+
return
|
132
|
+
|
133
|
+
if append:
|
134
|
+
out_element = document.createElement("div")
|
135
|
+
element.append(out_element)
|
136
|
+
else:
|
137
|
+
out_element = element.lastElementChild
|
138
|
+
if out_element is None:
|
139
|
+
out_element = element
|
140
|
+
|
141
|
+
if mime_type in ("application/javascript", "text/html"):
|
142
|
+
script_element = document.createRange().createContextualFragment(html)
|
143
|
+
out_element.append(script_element)
|
144
|
+
else:
|
145
|
+
out_element.innerHTML = html
|
146
|
+
|
147
|
+
|
148
|
+
def display(*values, target=None, append=True):
|
149
|
+
if target is None:
|
150
|
+
target = current_target()
|
151
|
+
|
152
|
+
element = document.getElementById(target)
|
153
|
+
for v in values:
|
154
|
+
_write(element, v, append=append)
|