@pyscript/core 0.1.17 → 0.1.19

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.
@@ -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
@@ -14,7 +14,7 @@ Accordingly, this is the bare minimum required output to bootstrap *PyScript Nex
14
14
 
15
15
  ```html
16
16
  <!-- Option 1: based on esm.sh which in turns is jsdlvr -->
17
- <script type="module" src="https://esm.sh/@pyscript/core@latest/core.js"></script>
17
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core"></script>
18
18
 
19
19
  <!-- Option 2: based on unpkg.com -->
20
20
  <script type="module" src="https://unpkg.com/@pyscript/core"></script>
@@ -28,22 +28,24 @@ If no `<script type="py">` or `<py-script>` tag is present, it is still possible
28
28
 
29
29
  ```html
30
30
  <script type="module">
31
- import { PyWorker } from "https://unpkg.com/@pyscript/core";
31
+ import { PyWorker } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
32
32
 
33
33
  const worker = PyWorker("./code.py", { config: "./config.toml" /* optional */ });
34
34
  </script>
35
35
  ```
36
36
 
37
+ Alternatively, it is possible to specify a `worker` attribute to either run embedded code or the provided `src` file.
38
+
37
39
  #### CSS
38
40
 
39
41
  If you are planning to use either `<py-config>` or `<py-script>` tags on the page, where latter case is usually better off with `<script type="py">` instead, you can also use CDNs to land our custom CSS:
40
42
 
41
43
  ```html
42
44
  <!-- Option 1: based on esm.sh which in turns is jsdlvr -->
43
- <link rel="stylesheet" href="https://esm.sh/@pyscript/core@latest/core.css">
45
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.css">
44
46
 
45
47
  <!-- Option 2: based on unpkg.com -->
46
- <link rel="stylesheet" href="https://unpkg.com/@pyscript/core/css">
48
+ <link rel="stylesheet" href="https://unpkg.com/@pyscript/core/dist/core.css">
47
49
 
48
50
  <!-- Option X: any CDN that uses npmjs registry should work -->
49
51
  ```
@@ -60,6 +62,30 @@ Once again, if you use `<script type="py">` instead, you won't need CSS unless y
60
62
  </script>
61
63
  ```
62
64
 
65
+ #### HTML Example
66
+
67
+ This is a complete reference to bootstrap *PyScript* in a HTML document.
68
+
69
+ ```html
70
+ <!doctype html>
71
+ <html lang="en">
72
+ <head>
73
+ <title>PyScript Next</title>
74
+ <meta charset="UTF-8">
75
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
76
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.css">
77
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core"></script>
78
+ </head>
79
+ <body>
80
+ <script type="py">
81
+ from pyscript import document
82
+
83
+ document.body.textContent = "PyScript Next"
84
+ </script>
85
+ </body>
86
+ </html>
87
+ ```
88
+
63
89
 
64
90
  ## Tag attributes API
65
91
 
@@ -81,7 +107,7 @@ The module itself is currently exporting the following utilities:
81
107
  * **hooks**, which allows plugins to define *ASAP* callbacks or strings that should be executed either in the main thread or the worker before, or after, the code has been executed.
82
108
 
83
109
  ```js
84
- import { hooks } from "https://unpkg.com/@pyscript/core";
110
+ import { hooks } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
85
111
 
86
112
  // example
87
113
  hooks.onInterpreterReady.add((utils, element) => {
@@ -146,7 +172,7 @@ The commonly shared utilities are:
146
172
 
147
173
  ```html
148
174
  <script type="module">
149
- import { PyWorker } from "https://unpkg.com/@pyscript/core";
175
+ import { PyWorker } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
150
176
 
151
177
  const worker = PyWorker("./worker.py");
152
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyscript/core",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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.1"
36
+ "polyscript": "^0.3.6"
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.0",
41
+ "rollup": "^3.29.1",
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,102 @@
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) config = pyConfig.getAttribute("src") || pyConfig.textContent;
52
+ else {
53
+ pyConfig = $('script[type="py"][config]');
54
+ if (pyConfig) config = pyConfig.getAttribute("config");
55
+ }
56
+ if (pyConfig) type = pyConfig.getAttribute("type");
57
+
58
+ // catch possible fetch errors
59
+ try {
60
+ const { json, toml, text, url } = await configDetails(config);
61
+ config = text;
62
+ if (json || type === "json") {
63
+ try {
64
+ parsed = JSON.parse(text);
65
+ } catch (e) {
66
+ error = syntaxError("JSON", url, e);
67
+ }
68
+ } else if (toml || type === "toml") {
69
+ try {
70
+ const { parse } = await import(
71
+ /* webpackIgnore: true */
72
+ "https://cdn.jsdelivr.net/npm/@webreflection/toml-j0.4/toml.js"
73
+ );
74
+ parsed = parse(text);
75
+ } catch (e) {
76
+ error = syntaxError("TOML", url, e);
77
+ }
78
+ }
79
+ } catch (e) {
80
+ error = e;
81
+ }
82
+
83
+ // parse all plugins and optionally ignore only
84
+ // those flagged as "undesired" via `!` prefix
85
+ const toBeAwaited = [];
86
+ for (const [key, value] of Object.entries(allPlugins)) {
87
+ if (error) {
88
+ if (key === "error") {
89
+ // show on page the config is broken, meaning that
90
+ // it was not possible to disable error plugin neither
91
+ // as that part wasn't correctly parsed anyway
92
+ value().then(({ notify }) => notify(error.message));
93
+ }
94
+ } else if (!parsed?.plugins?.includes(`!${key}`)) {
95
+ toBeAwaited.push(value());
96
+ }
97
+ }
98
+
99
+ // assign plugins as Promise.all only if needed
100
+ if (toBeAwaited.length) plugins = Promise.all(toBeAwaited);
101
+
102
+ export { config, plugins, error };
package/src/core.js CHANGED
@@ -1,42 +1,29 @@
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.
13
8
  import { queryTarget } from "../node_modules/polyscript/esm/script-handler.js";
14
- import { dedent } from "../node_modules/polyscript/esm/utils.js";
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 { robustFetch as fetch } from "./fetch.js";
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 getText = (body) => body.text();
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 = "py") => `${prefix}-${id++}`;
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
- 'Deprecated: use <script type="py"> for an always safe content parsing:\n',
60
+ `Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
74
61
  tag.innerHTML,
75
62
  );
76
63
 
@@ -140,89 +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
- define("py", {
149
- config,
150
- env: "py-script",
151
- interpreter: "pyodide",
152
- ...workerHooks,
153
- onWorkerReady(_, xworker) {
154
- assign(xworker.sync, sync);
155
- },
156
- onBeforeRun(pyodide, element) {
157
- currentElement = element;
158
- bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun");
159
- },
160
- onBeforeRunAsync(pyodide, element) {
161
- currentElement = element;
162
- bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRunAsync");
163
- },
164
- onAfterRun(pyodide, element) {
165
- bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun");
166
- },
167
- onAfterRunAsync(pyodide, element) {
168
- bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
169
- },
170
- async onInterpreterReady(pyodide, element) {
171
- if (shouldRegister) {
172
- shouldRegister = false;
173
- registerModule(pyodide);
174
- }
175
-
176
- // load plugins unless specified otherwise
177
- const toBeAwaited = [];
178
- for (const [key, value] of entries(plugins)) {
179
- if (!pyodide.config?.plugins?.includes(`!${key}`))
180
- toBeAwaited.push(value());
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
- if (isScript(element)) {
197
- const {
198
- attributes: { async: isAsync, target },
199
- } = element;
200
- const hasTarget = !!target?.value;
201
- const show = hasTarget
202
- ? queryTarget(target.value)
203
- : document.createElement("script-py");
204
-
205
- if (!hasTarget) {
206
- const { head, body } = document;
207
- if (head.contains(element)) body.append(show);
208
- 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);
209
164
  }
210
- if (!show.id) show.id = getID();
211
-
212
- // allows the code to retrieve the target element via
213
- // document.currentScript.target if needed
214
- defineProperty(element, "target", { value: show });
215
165
 
216
- pyodide[`run${isAsync ? "Async" : ""}`](
217
- await fetchSource(element, pyodide.io, true),
218
- );
219
- } else {
220
- // resolve PyScriptElement to allow connectedCallback
221
- element._pyodide.resolve(pyodide);
222
- }
223
- console.debug("[pyscript/main] PyScript Ready");
224
- },
225
- });
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
+ });
226
206
 
227
207
  class PyScriptElement extends HTMLElement {
228
208
  constructor() {
@@ -245,13 +225,16 @@ class PyScriptElement extends HTMLElement {
245
225
  const runner = this.hasAttribute("async") ? runAsync : run;
246
226
  this.srcCode = await fetchSource(this, io, !this.childElementCount);
247
227
  this.replaceChildren();
228
+ // notify before the code runs
229
+ dispatch(this, TYPE);
248
230
  runner(this.srcCode);
249
231
  this.style.display = "block";
250
232
  }
251
233
  }
252
234
  }
253
235
 
254
- customElements.define("py-script", PyScriptElement);
236
+ // define py-script only if the config didn't throw an error
237
+ error || customElements.define("py-script", PyScriptElement);
255
238
 
256
239
  /**
257
240
  * A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
package/src/fetch.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { FetchError, ErrorCode } from "./exceptions.js";
2
+ import { getText } from "../node_modules/polyscript/esm/fetch-utils.js";
3
+
4
+ export { getText };
2
5
 
3
6
  /**
4
7
  * This is a fetch wrapper that handles any non 200 responses and throws a
@@ -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
@@ -1,4 +1,4 @@
1
1
  // ⚠️ This file is an artifact: DO NOT MODIFY
2
2
  export default {
3
- error: () => import("./plugins/error.js"),
3
+ error: () => import(/* webpackIgnore: true */ "./plugins/error.js"),
4
4
  };
@@ -2,7 +2,8 @@
2
2
  export default {
3
3
  "_pyscript": {
4
4
  "__init__.py": "import js as window\n\nIS_WORKER = not hasattr(window, \"document\")\n\nif IS_WORKER:\n from polyscript import xworker as _xworker\n\n window = _xworker.window\n document = window.document\n sync = _xworker.sync\nelse:\n document = window.document\n",
5
- "display.py": "import base64\nimport html\nimport io\nimport re\n\nfrom . import document, window\n\n_MIME_METHODS = {\n \"__repr__\": \"text/plain\",\n \"_repr_html_\": \"text/html\",\n \"_repr_markdown_\": \"text/markdown\",\n \"_repr_svg_\": \"image/svg+xml\",\n \"_repr_png_\": \"image/png\",\n \"_repr_pdf_\": \"application/pdf\",\n \"_repr_jpeg_\": \"image/jpeg\",\n \"_repr_latex\": \"text/latex\",\n \"_repr_json_\": \"application/json\",\n \"_repr_javascript_\": \"application/javascript\",\n \"savefig\": \"image/png\",\n}\n\n\ndef _render_image(mime, value, meta):\n # If the image value is using bytes we should convert it to base64\n # otherwise it will return raw bytes and the browser will not be able to\n # render it.\n if isinstance(value, bytes):\n value = base64.b64encode(value).decode(\"utf-8\")\n\n # This is the pattern of base64 strings\n base64_pattern = re.compile(\n r\"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$\"\n )\n # If value doesn't match the base64 pattern we should encode it to base64\n if len(value) > 0 and not base64_pattern.match(value):\n value = base64.b64encode(value.encode(\"utf-8\")).decode(\"utf-8\")\n\n data = f\"data:{mime};charset=utf-8;base64,{value}\"\n attrs = \" \".join(['{k}=\"{v}\"' for k, v in meta.items()])\n return f'<img src=\"{data}\" {attrs}></img>'\n\n\ndef _identity(value, meta):\n return value\n\n\n_MIME_RENDERERS = {\n \"text/plain\": html.escape,\n \"text/html\": _identity,\n \"image/png\": lambda value, meta: _render_image(\"image/png\", value, meta),\n \"image/jpeg\": lambda value, meta: _render_image(\"image/jpeg\", value, meta),\n \"image/svg+xml\": _identity,\n \"application/json\": _identity,\n \"application/javascript\": lambda value, meta: f\"<script>{value}<\\\\/script>\",\n}\n\n\ndef _eval_formatter(obj, print_method):\n \"\"\"\n Evaluates a formatter method.\n \"\"\"\n if print_method == \"__repr__\":\n return repr(obj)\n elif hasattr(obj, print_method):\n if print_method == \"savefig\":\n buf = io.BytesIO()\n obj.savefig(buf, format=\"png\")\n buf.seek(0)\n return base64.b64encode(buf.read()).decode(\"utf-8\")\n return getattr(obj, print_method)()\n elif print_method == \"_repr_mimebundle_\":\n return {}, {}\n return None\n\n\ndef _format_mime(obj):\n \"\"\"\n Formats object using _repr_x_ methods.\n \"\"\"\n if isinstance(obj, str):\n return html.escape(obj), \"text/plain\"\n\n mimebundle = _eval_formatter(obj, \"_repr_mimebundle_\")\n if isinstance(mimebundle, tuple):\n format_dict, _ = mimebundle\n else:\n format_dict = mimebundle\n\n output, not_available = None, []\n for method, mime_type in reversed(_MIME_METHODS.items()):\n if mime_type in format_dict:\n output = format_dict[mime_type]\n else:\n output = _eval_formatter(obj, method)\n\n if output is None:\n continue\n elif mime_type not in _MIME_RENDERERS:\n not_available.append(mime_type)\n continue\n break\n if output is None:\n if not_available:\n window.console.warn(\n f\"Rendered object requested unavailable MIME renderers: {not_available}\"\n )\n output = repr(output)\n mime_type = \"text/plain\"\n elif isinstance(output, tuple):\n output, meta = output\n else:\n meta = {}\n return _MIME_RENDERERS[mime_type](output, meta), mime_type\n\n\ndef _write(element, value, append=False):\n html, mime_type = _format_mime(value)\n if html == \"\\\\n\":\n return\n\n if append:\n out_element = document.createElement(\"div\")\n element.append(out_element)\n else:\n out_element = element.lastElementChild\n if out_element is None:\n out_element = element\n\n if mime_type in (\"application/javascript\", \"text/html\"):\n script_element = document.createRange().createContextualFragment(html)\n out_element.append(script_element)\n else:\n out_element.innerHTML = html\n\n\ndef display(*values, target=None, append=True):\n element = document.getElementById(target)\n for v in values:\n _write(element, v, append=append)\n"
5
+ "display.py": "import base64\nimport html\nimport io\nimport re\n\nfrom . import document, window\n\n_MIME_METHODS = {\n \"__repr__\": \"text/plain\",\n \"_repr_html_\": \"text/html\",\n \"_repr_markdown_\": \"text/markdown\",\n \"_repr_svg_\": \"image/svg+xml\",\n \"_repr_png_\": \"image/png\",\n \"_repr_pdf_\": \"application/pdf\",\n \"_repr_jpeg_\": \"image/jpeg\",\n \"_repr_latex\": \"text/latex\",\n \"_repr_json_\": \"application/json\",\n \"_repr_javascript_\": \"application/javascript\",\n \"savefig\": \"image/png\",\n}\n\n\ndef _render_image(mime, value, meta):\n # If the image value is using bytes we should convert it to base64\n # otherwise it will return raw bytes and the browser will not be able to\n # render it.\n if isinstance(value, bytes):\n value = base64.b64encode(value).decode(\"utf-8\")\n\n # This is the pattern of base64 strings\n base64_pattern = re.compile(\n r\"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$\"\n )\n # If value doesn't match the base64 pattern we should encode it to base64\n if len(value) > 0 and not base64_pattern.match(value):\n value = base64.b64encode(value.encode(\"utf-8\")).decode(\"utf-8\")\n\n data = f\"data:{mime};charset=utf-8;base64,{value}\"\n attrs = \" \".join(['{k}=\"{v}\"' for k, v in meta.items()])\n return f'<img src=\"{data}\" {attrs}></img>'\n\n\ndef _identity(value, meta):\n return value\n\n\n_MIME_RENDERERS = {\n \"text/plain\": html.escape,\n \"text/html\": _identity,\n \"image/png\": lambda value, meta: _render_image(\"image/png\", value, meta),\n \"image/jpeg\": lambda value, meta: _render_image(\"image/jpeg\", value, meta),\n \"image/svg+xml\": _identity,\n \"application/json\": _identity,\n \"application/javascript\": lambda value, meta: f\"<script>{value}<\\\\/script>\",\n}\n\n\nclass HTML:\n \"\"\"\n Wrap a string so that display() can render it as plain HTML\n \"\"\"\n\n def __init__(self, html):\n self._html = html\n\n def _repr_html_(self):\n return self._html\n\n\ndef _eval_formatter(obj, print_method):\n \"\"\"\n Evaluates a formatter method.\n \"\"\"\n if print_method == \"__repr__\":\n return repr(obj)\n elif hasattr(obj, print_method):\n if print_method == \"savefig\":\n buf = io.BytesIO()\n obj.savefig(buf, format=\"png\")\n buf.seek(0)\n return base64.b64encode(buf.read()).decode(\"utf-8\")\n return getattr(obj, print_method)()\n elif print_method == \"_repr_mimebundle_\":\n return {}, {}\n return None\n\n\ndef _format_mime(obj):\n \"\"\"\n Formats object using _repr_x_ methods.\n \"\"\"\n if isinstance(obj, str):\n return html.escape(obj), \"text/plain\"\n\n mimebundle = _eval_formatter(obj, \"_repr_mimebundle_\")\n if isinstance(mimebundle, tuple):\n format_dict, _ = mimebundle\n else:\n format_dict = mimebundle\n\n output, not_available = None, []\n for method, mime_type in reversed(_MIME_METHODS.items()):\n if mime_type in format_dict:\n output = format_dict[mime_type]\n else:\n output = _eval_formatter(obj, method)\n\n if output is None:\n continue\n elif mime_type not in _MIME_RENDERERS:\n not_available.append(mime_type)\n continue\n break\n if output is None:\n if not_available:\n window.console.warn(\n f\"Rendered object requested unavailable MIME renderers: {not_available}\"\n )\n output = repr(output)\n mime_type = \"text/plain\"\n elif isinstance(output, tuple):\n output, meta = output\n else:\n meta = {}\n return _MIME_RENDERERS[mime_type](output, meta), mime_type\n\n\ndef _write(element, value, append=False):\n html, mime_type = _format_mime(value)\n if html == \"\\\\n\":\n return\n\n if append:\n out_element = document.createElement(\"div\")\n element.append(out_element)\n else:\n out_element = element.lastElementChild\n if out_element is None:\n out_element = element\n\n if mime_type in (\"application/javascript\", \"text/html\"):\n script_element = document.createRange().createContextualFragment(html)\n out_element.append(script_element)\n else:\n out_element.innerHTML = html\n\n\ndef display(*values, target=None, append=True):\n element = document.getElementById(target)\n for v in values:\n _write(element, v, append=append)\n",
6
+ "event_handling.py": "import inspect\n\nfrom pyodide.ffi.wrappers import add_event_listener\nfrom pyscript import document\n\n\ndef when(event_type=None, selector=None):\n \"\"\"\n Decorates a function and passes py-* events to the decorated function\n The events might or not be an argument of the decorated function\n \"\"\"\n\n def decorator(func):\n elements = document.querySelectorAll(selector)\n sig = inspect.signature(func)\n # Function doesn't receive events\n if not sig.parameters:\n\n def wrapper(*args, **kwargs):\n func()\n\n for el in elements:\n add_event_listener(el, event_type, wrapper)\n else:\n for el in elements:\n add_event_listener(el, event_type, func)\n return func\n\n return decorator\n"
6
7
  },
7
- "pyscript.py": "# export only what we want to expose as `pyscript` module\n# but not what is WORKER/MAIN dependent\nfrom _pyscript import window, document, IS_WORKER\nfrom _pyscript.display import display as _display\n\n# this part is needed to disambiguate between MAIN and WORKER\nif IS_WORKER:\n # in workers the display does not have a default ID\n # but there is a sync utility from xworker\n import polyscript as _polyscript\n from _pyscript import sync\n\n def current_target():\n return _polyscript.target\n\nelse:\n # in MAIN both PyWorker and current element target exist\n # so these are both exposed and the display will use,\n # if not specified otherwise, such current element target\n import _pyscript_js\n\n PyWorker = _pyscript_js.PyWorker\n\n def current_target():\n return _pyscript_js.target\n\n\n# the display provides a handy default target either in MAIN or WORKER\ndef display(*values, target=None, append=True):\n if target is None:\n target = current_target()\n\n return _display(*values, target=target, append=append)\n"
8
+ "pyscript.py": "# export only what we want to expose as `pyscript` module\n# but not what is WORKER/MAIN dependent\nfrom _pyscript import window, document, IS_WORKER\nfrom _pyscript.display import HTML, display as _display\nfrom _pyscript.event_handling import when\n\n# this part is needed to disambiguate between MAIN and WORKER\nif IS_WORKER:\n # in workers the display does not have a default ID\n # but there is a sync utility from xworker\n import polyscript as _polyscript\n from _pyscript import sync\n\n def current_target():\n return _polyscript.target\n\nelse:\n # in MAIN both PyWorker and current element target exist\n # so these are both exposed and the display will use,\n # if not specified otherwise, such current element target\n import _pyscript_js\n\n PyWorker = _pyscript_js.PyWorker\n\n def current_target():\n return _pyscript_js.target\n\n\n# the display provides a handy default target either in MAIN or WORKER\ndef display(*values, target=None, append=True):\n if target is None:\n target = current_target()\n\n return _display(*values, target=target, append=append)\n"
8
9
  };
@@ -0,0 +1,3 @@
1
+ export let config: any;
2
+ export let plugins: any;
3
+ export let error: any;
package/types/core.d.ts CHANGED
@@ -21,6 +21,5 @@ export namespace hooks {
21
21
  let codeAfterRunWorker: Set<string>;
22
22
  let codeAfterRunWorkerAsync: Set<string>;
23
23
  }
24
- declare let config: any;
24
+ import { config } from "./config.js";
25
25
  import sync from "./sync.js";
26
- export {};
package/types/fetch.d.ts CHANGED
@@ -8,3 +8,4 @@
8
8
  * @returns {Promise<Response>}
9
9
  */
10
10
  export function robustFetch(url: string, options?: Request): Promise<Response>;
11
+ export { getText };
@@ -1 +1 @@
1
- export {};
1
+ export function notify(message: any): void;
@@ -2,6 +2,7 @@ declare const _default: {
2
2
  _pyscript: {
3
3
  "__init__.py": string;
4
4
  "display.py": string;
5
+ "event_handling.py": string;
5
6
  };
6
7
  "pyscript.py": string;
7
8
  };
@@ -1,2 +0,0 @@
1
- import{hooks as e}from"./core.js";function n(e){const n=document.createElement("div");n.className="py-error",n.textContent=e,n.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(n)}e.onBeforeRun.add((function o(r){e.onBeforeRun.delete(o);const{stderr:t}=r.io;r.io.stderr=(e,...o)=>(n(e.message||e),t(e,...o)),addEventListener("error",(({message:e})=>{e.startsWith("Uncaught PythonError")&&n(e)}))}));
2
- //# sourceMappingURL=error-91f1c2f6.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"error-91f1c2f6.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\nfunction 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":"kCA0BA,SAASA,EAAOC,GACZ,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"}