[next] Add mpy as custom type with PyScript magic attached (#1728)

This commit is contained in:
Andrea Giammarchi
2023-09-20 18:06:52 +02:00
committed by GitHub
parent 8f3c36deea
commit ad0dde3f17
6 changed files with 207 additions and 150 deletions

View File

@@ -28,13 +28,14 @@ repos:
rev: v0.0.257 rev: v0.0.257
hooks: hooks:
- id: ruff - id: ruff
exclude: pyscript\.core/test|pyscript\.core/dist|pyscript.core/src/stdlib/pyscript.py exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py|pyscript\.core/test|pyscript\.core/dist|pyscript\.core/src/stdlib/pyscript\.py
args: [--fix] args: [--fix]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 23.1.0
hooks: hooks:
- id: black - id: black
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.4 rev: v2.2.4

View File

@@ -1,4 +1,6 @@
py-script, py-script,
py-config { py-config,
mpy-script,
mpy-config {
display: none; display: none;
} }

View File

@@ -15,16 +15,15 @@ import stdlib from "./stdlib.js";
import { config, plugins, error } from "./config.js"; import { config, plugins, error } from "./config.js";
import { robustFetch as fetch, getText } from "./fetch.js"; import { robustFetch as fetch, getText } from "./fetch.js";
const { assign, defineProperty, entries } = Object; const { assign, defineProperty } = Object;
const TYPE = "py";
// allows lazy element features on code evaluation // allows lazy element features on code evaluation
let currentElement; let currentElement;
// create a unique identifier when/if needed const TYPES = new Map([
let id = 0; ["py", "pyodide"],
const getID = (prefix = TYPE) => `${prefix}-${id++}`; ["mpy", "micropython"],
]);
// generic helper to disambiguate between custom element and script // generic helper to disambiguate between custom element and script
const isScript = ({ tagName }) => tagName === "SCRIPT"; const isScript = ({ tagName }) => tagName === "SCRIPT";
@@ -41,35 +40,11 @@ const after = () => {
delete document.currentScript; delete document.currentScript;
}; };
/**
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
* It either throws an error if the 'src' can't be fetched or it returns a fallback
* content as source.
*/
const fetchSource = async (tag, io, asText) => {
if (tag.hasAttribute("src")) {
try {
return await fetch(tag.getAttribute("src")).then(getText);
} catch (error) {
io.stderr(error);
}
}
if (asText) return dedent(tag.textContent);
console.warn(
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
tag.innerHTML,
);
return dedent(tag.innerHTML);
};
// common life-cycle handlers for any node // common life-cycle handlers for any node
const bootstrapNodeAndPlugins = (pyodide, element, callback, hook) => { const bootstrapNodeAndPlugins = (wrap, element, callback, hook) => {
// make it possible to reach the current target node via Python // make it possible to reach the current target node via Python
callback(element); callback(element);
for (const fn of hooks[hook]) fn(pyodide, element); for (const fn of hooks[hook]) fn(wrap, element);
}; };
let shouldRegister = true; let shouldRegister = true;
@@ -128,16 +103,45 @@ const workerHooks = {
[...hooks.codeAfterRunWorkerAsync].map(dedent).join("\n"), [...hooks.codeAfterRunWorkerAsync].map(dedent).join("\n"),
}; };
// possible early errors sent by polyscript for (const [TYPE, interpreter] of TYPES) {
const errors = new Map(); // create a unique identifier when/if needed
let id = 0;
const getID = (prefix = TYPE) => `${prefix}-${id++}`;
/**
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
* It either throws an error if the 'src' can't be fetched or it returns a fallback
* content as source.
*/
const fetchSource = async (tag, io, asText) => {
if (tag.hasAttribute("src")) {
try {
return await fetch(tag.getAttribute("src")).then(getText);
} catch (error) {
io.stderr(error);
}
}
if (asText) return dedent(tag.textContent);
console.warn(
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
tag.innerHTML,
);
return dedent(tag.innerHTML);
};
// define the module as both `<script type="py">` and `<py-script>`
// but only if the config didn't throw an error
if (!error) {
// possible early errors sent by polyscript
const errors = new Map();
// define the module as both `<script type="py">` and `<py-script>`
// but only if the config didn't throw an error
error ||
define(TYPE, { define(TYPE, {
config, config,
interpreter,
env: `${TYPE}-script`, env: `${TYPE}-script`,
interpreter: "pyodide",
onerror(error, element) { onerror(error, element) {
errors.set(element, error); errors.set(element, error);
}, },
@@ -145,29 +149,34 @@ error ||
onWorkerReady(_, xworker) { onWorkerReady(_, xworker) {
assign(xworker.sync, sync); assign(xworker.sync, sync);
}, },
onBeforeRun(pyodide, element) { onBeforeRun(wrap, element) {
currentElement = element; currentElement = element;
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun"); bootstrapNodeAndPlugins(wrap, element, before, "onBeforeRun");
}, },
onBeforeRunAsync(pyodide, element) { onBeforeRunAsync(wrap, element) {
currentElement = element; currentElement = element;
bootstrapNodeAndPlugins( bootstrapNodeAndPlugins(
pyodide, wrap,
element, element,
before, before,
"onBeforeRunAsync", "onBeforeRunAsync",
); );
}, },
onAfterRun(pyodide, element) { onAfterRun(wrap, element) {
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun"); bootstrapNodeAndPlugins(wrap, element, after, "onAfterRun");
}, },
onAfterRunAsync(pyodide, element) { onAfterRunAsync(wrap, element) {
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync"); bootstrapNodeAndPlugins(
wrap,
element,
after,
"onAfterRunAsync",
);
}, },
async onInterpreterReady(pyodide, element) { async onInterpreterReady(wrap, element) {
if (shouldRegister) { if (shouldRegister) {
shouldRegister = false; shouldRegister = false;
registerModule(pyodide); registerModule(wrap);
} }
// ensure plugins are bootstrapped already // ensure plugins are bootstrapped already
@@ -176,7 +185,7 @@ error ||
// allows plugins to do whatever they want with the element // allows plugins to do whatever they want with the element
// before regular stuff happens in here // before regular stuff happens in here
for (const callback of hooks.onInterpreterReady) for (const callback of hooks.onInterpreterReady)
callback(pyodide, element); callback(wrap, element);
// now that all possible plugins are configured, // now that all possible plugins are configured,
// bail out if polyscript encountered an error // bail out if polyscript encountered an error
@@ -186,7 +195,7 @@ error ||
const clone = message === INVALID_CONTENT; const clone = message === INVALID_CONTENT;
message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `; message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
message += element.cloneNode(clone).outerHTML; message += element.cloneNode(clone).outerHTML;
pyodide.io.stderr(message); wrap.io.stderr(message);
return; return;
} }
@@ -212,25 +221,31 @@ error ||
// notify before the code runs // notify before the code runs
dispatch(element, TYPE); dispatch(element, TYPE);
pyodide[`run${isAsync ? "Async" : ""}`]( wrap[`run${isAsync ? "Async" : ""}`](
await fetchSource(element, pyodide.io, true), await fetchSource(element, wrap.io, true),
); );
} else { } else {
// resolve PyScriptElement to allow connectedCallback // resolve PyScriptElement to allow connectedCallback
element._pyodide.resolve(pyodide); element._wrap.resolve(wrap);
} }
console.debug("[pyscript/main] PyScript Ready"); console.debug("[pyscript/main] PyScript Ready");
}, },
}); });
}
class PyScriptElement extends HTMLElement { class PyScriptElement extends HTMLElement {
constructor() { constructor() {
assign(super(), { assign(super(), {
_pyodide: Promise.withResolvers(), _wrap: Promise.withResolvers(),
srcCode: "", srcCode: "",
executed: false, executed: false,
}); });
} }
get _pyodide() {
// TODO: deprecate this hidden attribute already
// currently used by integration tests
return this._wrap;
}
get id() { get id() {
return super.id || (super.id = getID()); return super.id || (super.id = getID());
} }
@@ -240,9 +255,13 @@ class PyScriptElement extends HTMLElement {
async connectedCallback() { async connectedCallback() {
if (!this.executed) { if (!this.executed) {
this.executed = true; this.executed = true;
const { io, run, runAsync } = await this._pyodide.promise; const { io, run, runAsync } = await this._wrap.promise;
const runner = this.hasAttribute("async") ? runAsync : run; const runner = this.hasAttribute("async") ? runAsync : run;
this.srcCode = await fetchSource(this, io, !this.childElementCount); this.srcCode = await fetchSource(
this,
io,
!this.childElementCount,
);
this.replaceChildren(); this.replaceChildren();
// notify before the code runs // notify before the code runs
dispatch(this, TYPE); dispatch(this, TYPE);
@@ -250,10 +269,14 @@ class PyScriptElement extends HTMLElement {
this.style.display = "block"; this.style.display = "block";
} }
} }
}
// define py-script only if the config didn't throw an error
if (!error) customElements.define(`${TYPE}-script`, PyScriptElement);
} }
// define py-script only if the config didn't throw an error // TBD: I think manual worker cases are interesting in pyodide only
error || customElements.define("py-script", PyScriptElement); // so for the time being we should be fine with this export.
/** /**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module. * A `Worker` facade able to bootstrap on the worker thread only a PyScript module.

View File

@@ -31,4 +31,13 @@
from pyscript.magic_js import RUNNING_IN_WORKER, window, document, sync from pyscript.magic_js import RUNNING_IN_WORKER, window, document, sync
from pyscript.display import HTML, display from pyscript.display import HTML, display
from pyscript.event_handling import when
try:
from pyscript.event_handling import when
except:
from pyscript.util import NotSupported
when = NotSupported(
"pyscript.when",
"pyscript.when currently not available with this interpreter"
)

View File

@@ -5,12 +5,11 @@ class NotSupported:
""" """
def __init__(self, name, error): def __init__(self, name, error):
# we set attributes using self.__dict__ to bypass the __setattr__ object.__setattr__(self, "name", name)
self.__dict__['name'] = name object.__setattr__(self, "error", error)
self.__dict__['error'] = error
def __repr__(self): def __repr__(self):
return f'<NotSupported {self.name} [{self.error}]>' return f"<NotSupported {self.name} [{self.error}]>"
def __getattr__(self, attr): def __getattr__(self, attr):
raise AttributeError(self.error) raise AttributeError(self.error)

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript Next</title>
<script>
addEventListener("mpy:ready", console.log);
</script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="mpy">
from pyscript import display
display("Hello", "M-PyScript Next", append=False)
</script>
<mpy-script worker>
from pyscript import display
display("Hello", "M-PyScript Next Worker", append=False)
</mpy-script>
</body>
</html>