mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
[next] Add mpy as custom type with PyScript magic attached (#1728)
This commit is contained in:
committed by
GitHub
parent
8f3c36deea
commit
ad0dde3f17
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
py-script,
|
py-script,
|
||||||
py-config {
|
py-config,
|
||||||
|
mpy-script,
|
||||||
|
mpy-config {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,132 +103,180 @@ 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++}`;
|
||||||
|
|
||||||
// define the module as both `<script type="py">` and `<py-script>`
|
/**
|
||||||
// but only if the config didn't throw an error
|
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
||||||
error ||
|
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
||||||
define(TYPE, {
|
* content as source.
|
||||||
config,
|
*/
|
||||||
env: `${TYPE}-script`,
|
const fetchSource = async (tag, io, asText) => {
|
||||||
interpreter: "pyodide",
|
if (tag.hasAttribute("src")) {
|
||||||
onerror(error, element) {
|
try {
|
||||||
errors.set(element, error);
|
return await fetch(tag.getAttribute("src")).then(getText);
|
||||||
},
|
} catch (error) {
|
||||||
...workerHooks,
|
io.stderr(error);
|
||||||
onWorkerReady(_, xworker) {
|
|
||||||
assign(xworker.sync, sync);
|
|
||||||
},
|
|
||||||
onBeforeRun(pyodide, element) {
|
|
||||||
currentElement = element;
|
|
||||||
bootstrapNodeAndPlugins(pyodide, element, before, "onBeforeRun");
|
|
||||||
},
|
|
||||||
onBeforeRunAsync(pyodide, element) {
|
|
||||||
currentElement = element;
|
|
||||||
bootstrapNodeAndPlugins(
|
|
||||||
pyodide,
|
|
||||||
element,
|
|
||||||
before,
|
|
||||||
"onBeforeRunAsync",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAfterRun(pyodide, element) {
|
|
||||||
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRun");
|
|
||||||
},
|
|
||||||
onAfterRunAsync(pyodide, element) {
|
|
||||||
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
|
||||||
},
|
|
||||||
async onInterpreterReady(pyodide, element) {
|
|
||||||
if (shouldRegister) {
|
|
||||||
shouldRegister = false;
|
|
||||||
registerModule(pyodide);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ensure plugins are bootstrapped already
|
if (asText) return dedent(tag.textContent);
|
||||||
if (plugins) await plugins;
|
|
||||||
|
|
||||||
// allows plugins to do whatever they want with the element
|
console.warn(
|
||||||
// before regular stuff happens in here
|
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
|
||||||
for (const callback of hooks.onInterpreterReady)
|
tag.innerHTML,
|
||||||
callback(pyodide, element);
|
);
|
||||||
|
|
||||||
// now that all possible plugins are configured,
|
return dedent(tag.innerHTML);
|
||||||
// bail out if polyscript encountered an error
|
};
|
||||||
if (errors.has(element)) {
|
|
||||||
let { message } = errors.get(element);
|
|
||||||
errors.delete(element);
|
|
||||||
const clone = message === INVALID_CONTENT;
|
|
||||||
message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
|
|
||||||
message += element.cloneNode(clone).outerHTML;
|
|
||||||
pyodide.io.stderr(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isScript(element)) {
|
// define the module as both `<script type="py">` and `<py-script>`
|
||||||
const {
|
// but only if the config didn't throw an error
|
||||||
attributes: { async: isAsync, target },
|
if (!error) {
|
||||||
} = element;
|
// possible early errors sent by polyscript
|
||||||
const hasTarget = !!target?.value;
|
const errors = new Map();
|
||||||
const show = hasTarget
|
|
||||||
? queryTarget(target.value)
|
|
||||||
: document.createElement("script-py");
|
|
||||||
|
|
||||||
if (!hasTarget) {
|
define(TYPE, {
|
||||||
const { head, body } = document;
|
config,
|
||||||
if (head.contains(element)) body.append(show);
|
interpreter,
|
||||||
else element.after(show);
|
env: `${TYPE}-script`,
|
||||||
}
|
onerror(error, element) {
|
||||||
if (!show.id) show.id = getID();
|
errors.set(element, error);
|
||||||
|
},
|
||||||
// allows the code to retrieve the target element via
|
...workerHooks,
|
||||||
// document.currentScript.target if needed
|
onWorkerReady(_, xworker) {
|
||||||
defineProperty(element, "target", { value: show });
|
assign(xworker.sync, sync);
|
||||||
|
},
|
||||||
// notify before the code runs
|
onBeforeRun(wrap, element) {
|
||||||
dispatch(element, TYPE);
|
currentElement = element;
|
||||||
pyodide[`run${isAsync ? "Async" : ""}`](
|
bootstrapNodeAndPlugins(wrap, element, before, "onBeforeRun");
|
||||||
await fetchSource(element, pyodide.io, true),
|
},
|
||||||
|
onBeforeRunAsync(wrap, element) {
|
||||||
|
currentElement = element;
|
||||||
|
bootstrapNodeAndPlugins(
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
before,
|
||||||
|
"onBeforeRunAsync",
|
||||||
);
|
);
|
||||||
} else {
|
},
|
||||||
// resolve PyScriptElement to allow connectedCallback
|
onAfterRun(wrap, element) {
|
||||||
element._pyodide.resolve(pyodide);
|
bootstrapNodeAndPlugins(wrap, element, after, "onAfterRun");
|
||||||
}
|
},
|
||||||
console.debug("[pyscript/main] PyScript Ready");
|
onAfterRunAsync(wrap, element) {
|
||||||
},
|
bootstrapNodeAndPlugins(
|
||||||
});
|
wrap,
|
||||||
|
element,
|
||||||
|
after,
|
||||||
|
"onAfterRunAsync",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async onInterpreterReady(wrap, element) {
|
||||||
|
if (shouldRegister) {
|
||||||
|
shouldRegister = false;
|
||||||
|
registerModule(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
class PyScriptElement extends HTMLElement {
|
// ensure plugins are bootstrapped already
|
||||||
constructor() {
|
if (plugins) await plugins;
|
||||||
assign(super(), {
|
|
||||||
_pyodide: Promise.withResolvers(),
|
// allows plugins to do whatever they want with the element
|
||||||
srcCode: "",
|
// before regular stuff happens in here
|
||||||
executed: false,
|
for (const callback of hooks.onInterpreterReady)
|
||||||
|
callback(wrap, element);
|
||||||
|
|
||||||
|
// now that all possible plugins are configured,
|
||||||
|
// bail out if polyscript encountered an error
|
||||||
|
if (errors.has(element)) {
|
||||||
|
let { message } = errors.get(element);
|
||||||
|
errors.delete(element);
|
||||||
|
const clone = message === INVALID_CONTENT;
|
||||||
|
message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
|
||||||
|
message += element.cloneNode(clone).outerHTML;
|
||||||
|
wrap.io.stderr(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScript(element)) {
|
||||||
|
const {
|
||||||
|
attributes: { async: isAsync, target },
|
||||||
|
} = element;
|
||||||
|
const hasTarget = !!target?.value;
|
||||||
|
const show = hasTarget
|
||||||
|
? queryTarget(target.value)
|
||||||
|
: document.createElement("script-py");
|
||||||
|
|
||||||
|
if (!hasTarget) {
|
||||||
|
const { head, body } = document;
|
||||||
|
if (head.contains(element)) body.append(show);
|
||||||
|
else element.after(show);
|
||||||
|
}
|
||||||
|
if (!show.id) show.id = getID();
|
||||||
|
|
||||||
|
// allows the code to retrieve the target element via
|
||||||
|
// document.currentScript.target if needed
|
||||||
|
defineProperty(element, "target", { value: show });
|
||||||
|
|
||||||
|
// notify before the code runs
|
||||||
|
dispatch(element, TYPE);
|
||||||
|
wrap[`run${isAsync ? "Async" : ""}`](
|
||||||
|
await fetchSource(element, wrap.io, true),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// resolve PyScriptElement to allow connectedCallback
|
||||||
|
element._wrap.resolve(wrap);
|
||||||
|
}
|
||||||
|
console.debug("[pyscript/main] PyScript Ready");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
get id() {
|
|
||||||
return super.id || (super.id = getID());
|
class PyScriptElement extends HTMLElement {
|
||||||
}
|
constructor() {
|
||||||
set id(value) {
|
assign(super(), {
|
||||||
super.id = value;
|
_wrap: Promise.withResolvers(),
|
||||||
}
|
srcCode: "",
|
||||||
async connectedCallback() {
|
executed: false,
|
||||||
if (!this.executed) {
|
});
|
||||||
this.executed = true;
|
}
|
||||||
const { io, run, runAsync } = await this._pyodide.promise;
|
get _pyodide() {
|
||||||
const runner = this.hasAttribute("async") ? runAsync : run;
|
// TODO: deprecate this hidden attribute already
|
||||||
this.srcCode = await fetchSource(this, io, !this.childElementCount);
|
// currently used by integration tests
|
||||||
this.replaceChildren();
|
return this._wrap;
|
||||||
// notify before the code runs
|
}
|
||||||
dispatch(this, TYPE);
|
get id() {
|
||||||
runner(this.srcCode);
|
return super.id || (super.id = getID());
|
||||||
this.style.display = "block";
|
}
|
||||||
|
set id(value) {
|
||||||
|
super.id = value;
|
||||||
|
}
|
||||||
|
async connectedCallback() {
|
||||||
|
if (!this.executed) {
|
||||||
|
this.executed = true;
|
||||||
|
const { io, run, runAsync } = await this._wrap.promise;
|
||||||
|
const runner = this.hasAttribute("async") ? runAsync : run;
|
||||||
|
this.srcCode = await fetchSource(
|
||||||
|
this,
|
||||||
|
io,
|
||||||
|
!this.childElementCount,
|
||||||
|
);
|
||||||
|
this.replaceChildren();
|
||||||
|
// notify before the code runs
|
||||||
|
dispatch(this, TYPE);
|
||||||
|
runner(this.srcCode);
|
||||||
|
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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
23
pyscript.core/test/mpy.html
Normal file
23
pyscript.core/test/mpy.html
Normal 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>
|
||||||
Reference in New Issue
Block a user