mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
[next] Bring in the good old PyScript display (#1628)
This commit is contained in:
committed by
GitHub
parent
27c91e9703
commit
84dcde188b
@@ -27,7 +27,7 @@ repos:
|
|||||||
rev: v0.0.257
|
rev: v0.0.257
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
exclude: pyscript\.core/test
|
exclude: pyscript\.core/test|pyscript\.core/src/display.py
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
14
pyscript.core/package-lock.json
generated
14
pyscript.core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"license": "APACHE-2.0",
|
"license": "APACHE-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ungap/with-resolvers": "^0.1.0",
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@rollup/plugin-terser": "^0.4.3",
|
"@rollup/plugin-terser": "^0.4.3",
|
||||||
"rollup": "^3.27.2",
|
"rollup": "^3.27.2",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.4.2",
|
"static-handler": "^0.4.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
}
|
}
|
||||||
@@ -1591,6 +1592,15 @@
|
|||||||
"postcss": "8.x"
|
"postcss": "8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rollup-plugin-string": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-string/-/rollup-plugin-string-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-vqyzgn9QefAgeKi+Y4A7jETeIAU1zQmS6VotH6bzm/zmUQEnYkpIGRaOBPY41oiWYV4JyBoGAaBjYMYuv+6wVw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"rollup-pluginutils": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup-pluginutils": {
|
"node_modules/rollup-pluginutils": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"main": "core.js",
|
"main": "core.js",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@rollup/plugin-terser": "^0.4.3",
|
"@rollup/plugin-terser": "^0.4.3",
|
||||||
"rollup": "^3.27.2",
|
"rollup": "^3.27.2",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.4.2",
|
"static-handler": "^0.4.2",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,10 +3,20 @@
|
|||||||
|
|
||||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||||
import terser from "@rollup/plugin-terser";
|
import terser from "@rollup/plugin-terser";
|
||||||
|
import { string } from "rollup-plugin-string";
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
string({
|
||||||
|
// Required to be specified
|
||||||
|
include: "**/*.py",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "./src/core.js",
|
input: "./src/core.js",
|
||||||
plugins: process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
|
||||||
|
),
|
||||||
output: {
|
output: {
|
||||||
esModule: true,
|
esModule: true,
|
||||||
file: "./core.js",
|
file: "./core.js",
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import "@ungap/with-resolvers";
|
|||||||
import { $ } from "basic-devtools";
|
import { $ } from "basic-devtools";
|
||||||
import { define, XWorker } from "polyscript";
|
import { define, XWorker } from "polyscript";
|
||||||
|
|
||||||
|
// this is imported as string (via rollup)
|
||||||
|
import display from "./display.py";
|
||||||
|
|
||||||
// TODO: this is not strictly polyscript related but handy ... not sure
|
// TODO: this is not strictly polyscript related but handy ... not sure
|
||||||
// we should factor this utility out a part but this works anyway.
|
// we should factor this utility out a part but this works anyway.
|
||||||
import { queryTarget } from "../node_modules/polyscript/esm/script-handler.js";
|
import { queryTarget } from "../node_modules/polyscript/esm/script-handler.js";
|
||||||
@@ -79,6 +82,17 @@ const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
|||||||
worker.onerror = ({ error }) => io.stderr(error);
|
worker.onerror = ({ error }) => io.stderr(error);
|
||||||
return worker;
|
return worker;
|
||||||
}
|
}
|
||||||
|
// trap once the python `display` utility (borrowed from "classic PyScript")
|
||||||
|
// provide the regular Pyodide globals instead of those from xworker
|
||||||
|
const pyDisplay = interpreter.runPython(
|
||||||
|
[
|
||||||
|
"import js",
|
||||||
|
"document=js.document",
|
||||||
|
"window=js",
|
||||||
|
display,
|
||||||
|
"display",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
interpreter.registerJsModule("pyscript", {
|
interpreter.registerJsModule("pyscript", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
document,
|
document,
|
||||||
@@ -91,11 +105,8 @@ const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
|||||||
? currentElement.target.id
|
? currentElement.target.id
|
||||||
: currentElement.id;
|
: currentElement.id;
|
||||||
|
|
||||||
// TODO: decide which feature of display we want to keep
|
|
||||||
return (what, target = id, append = true) => {
|
return (what, target = id, append = true) => {
|
||||||
const element = document.getElementById(target);
|
pyDisplay.callKwargs(...[].concat(what), { target, append });
|
||||||
if (append) element.append(what);
|
|
||||||
else element.textContent = what;
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -125,13 +136,16 @@ export const hooks = {
|
|||||||
|
|
||||||
const workerPyScriptModule = [
|
const workerPyScriptModule = [
|
||||||
"from pyodide_js import FS",
|
"from pyodide_js import FS",
|
||||||
`FS.writeFile('./pyscript.py', '${[
|
`FS.writeFile('./pyscript.py', ${JSON.stringify(
|
||||||
"import polyscript",
|
[
|
||||||
"document=polyscript.xworker.window.document",
|
"import polyscript",
|
||||||
"window=polyscript.xworker.window",
|
"document=polyscript.xworker.window.document",
|
||||||
"sync=polyscript.xworker.sync",
|
"window=polyscript.xworker.window",
|
||||||
].join(";")}')`,
|
"sync=polyscript.xworker.sync",
|
||||||
].join(";");
|
display,
|
||||||
|
].join("\n"),
|
||||||
|
)})`,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
const workerHooks = {
|
const workerHooks = {
|
||||||
codeBeforeRunWorker: () =>
|
codeBeforeRunWorker: () =>
|
||||||
|
|||||||
142
pyscript.core/src/display.py
Normal file
142
pyscript.core/src/display.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# ⚠️ WARNING - both `document` and `window` are added at runtime
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import html
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
_MIME_METHODS = {
|
||||||
|
"__repr__": "text/plain",
|
||||||
|
"_repr_html_": "text/html",
|
||||||
|
"_repr_markdown_": "text/markdown",
|
||||||
|
"_repr_svg_": "image/svg+xml",
|
||||||
|
"_repr_png_": "image/png",
|
||||||
|
"_repr_pdf_": "application/pdf",
|
||||||
|
"_repr_jpeg_": "image/jpeg",
|
||||||
|
"_repr_latex": "text/latex",
|
||||||
|
"_repr_json_": "application/json",
|
||||||
|
"_repr_javascript_": "application/javascript",
|
||||||
|
"savefig": "image/png",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _render_image(mime, value, meta):
|
||||||
|
# If the image value is using bytes we should convert it to base64
|
||||||
|
# otherwise it will return raw bytes and the browser will not be able to
|
||||||
|
# render it.
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = base64.b64encode(value).decode("utf-8")
|
||||||
|
|
||||||
|
# This is the pattern of base64 strings
|
||||||
|
base64_pattern = re.compile(
|
||||||
|
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
||||||
|
)
|
||||||
|
# If value doesn't match the base64 pattern we should encode it to base64
|
||||||
|
if len(value) > 0 and not base64_pattern.match(value):
|
||||||
|
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
data = f"data:{mime};charset=utf-8;base64,{value}"
|
||||||
|
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
||||||
|
return f'<img src="{data}" {attrs}></img>'
|
||||||
|
|
||||||
|
|
||||||
|
def _identity(value, meta):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
_MIME_RENDERERS = {
|
||||||
|
"text/plain": html.escape,
|
||||||
|
"text/html": _identity,
|
||||||
|
"image/png": lambda value, meta: _render_image("image/png", value, meta),
|
||||||
|
"image/jpeg": lambda value, meta: _render_image("image/jpeg", value, meta),
|
||||||
|
"image/svg+xml": _identity,
|
||||||
|
"application/json": _identity,
|
||||||
|
"application/javascript": lambda value, meta: f"<script>{value}<\\/script>",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_formatter(obj, print_method):
|
||||||
|
"""
|
||||||
|
Evaluates a formatter method.
|
||||||
|
"""
|
||||||
|
if print_method == "__repr__":
|
||||||
|
return repr(obj)
|
||||||
|
elif hasattr(obj, print_method):
|
||||||
|
if print_method == "savefig":
|
||||||
|
buf = io.BytesIO()
|
||||||
|
obj.savefig(buf, format="png")
|
||||||
|
buf.seek(0)
|
||||||
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
|
return getattr(obj, print_method)()
|
||||||
|
elif print_method == "_repr_mimebundle_":
|
||||||
|
return {}, {}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_mime(obj):
|
||||||
|
"""
|
||||||
|
Formats object using _repr_x_ methods.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return html.escape(obj), "text/plain"
|
||||||
|
|
||||||
|
mimebundle = _eval_formatter(obj, "_repr_mimebundle_")
|
||||||
|
if isinstance(mimebundle, tuple):
|
||||||
|
format_dict, _ = mimebundle
|
||||||
|
else:
|
||||||
|
format_dict = mimebundle
|
||||||
|
|
||||||
|
output, not_available = None, []
|
||||||
|
for method, mime_type in reversed(_MIME_METHODS.items()):
|
||||||
|
if mime_type in format_dict:
|
||||||
|
output = format_dict[mime_type]
|
||||||
|
else:
|
||||||
|
output = _eval_formatter(obj, method)
|
||||||
|
|
||||||
|
if output is None:
|
||||||
|
continue
|
||||||
|
elif mime_type not in _MIME_RENDERERS:
|
||||||
|
not_available.append(mime_type)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if output is None:
|
||||||
|
if not_available:
|
||||||
|
window.console.warn(
|
||||||
|
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||||
|
)
|
||||||
|
output = repr(output)
|
||||||
|
mime_type = "text/plain"
|
||||||
|
elif isinstance(output, tuple):
|
||||||
|
output, meta = output
|
||||||
|
else:
|
||||||
|
meta = {}
|
||||||
|
return _MIME_RENDERERS[mime_type](output, meta), mime_type
|
||||||
|
|
||||||
|
|
||||||
|
def _write(element, value, append=False):
|
||||||
|
global _pyscript_id
|
||||||
|
|
||||||
|
html, mime_type = _format_mime(value)
|
||||||
|
if html == "\\n":
|
||||||
|
return
|
||||||
|
|
||||||
|
if append:
|
||||||
|
out_element = document.createElement("div")
|
||||||
|
element.append(out_element)
|
||||||
|
else:
|
||||||
|
out_element = element.lastElementChild
|
||||||
|
if out_element is None:
|
||||||
|
out_element = element
|
||||||
|
|
||||||
|
if mime_type in ("application/javascript", "text/html"):
|
||||||
|
script_element = document.createRange().createContextualFragment(html)
|
||||||
|
out_element.append(script_element)
|
||||||
|
else:
|
||||||
|
out_element.innerHTML = html
|
||||||
|
|
||||||
|
|
||||||
|
def display(*values, target=None, append=True):
|
||||||
|
element = document.getElementById(target)
|
||||||
|
for v in values:
|
||||||
|
_write(element, v, append=append)
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
<script type="module" src="../core.js"></script>
|
<script type="module" src="../core.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<py-script>display("Hello PyScript Next")</py-script>
|
<script type="py">
|
||||||
|
from pyscript import display
|
||||||
|
display("Hello PyScript Next")
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,10 +6,6 @@
|
|||||||
<title>PyScript Next</title>
|
<title>PyScript Next</title>
|
||||||
<link rel="stylesheet" href="../core.css" />
|
<link rel="stylesheet" href="../core.css" />
|
||||||
|
|
||||||
<!-- the worker attribute -->
|
|
||||||
<script type="module" src="../core.js"></script>
|
|
||||||
<script type="py" worker="./worker.py" config="./config.json"></script>
|
|
||||||
|
|
||||||
<!-- the PyWorker approach -->
|
<!-- the PyWorker approach -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { PyWorker } from '../core.js';
|
import { PyWorker } from '../core.js';
|
||||||
@@ -17,8 +13,12 @@
|
|||||||
// the type is overwritten as "pyodide" in PyScript as the module
|
// the type is overwritten as "pyodide" in PyScript as the module
|
||||||
// lives in that env too
|
// lives in that env too
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- the worker attribute -->
|
||||||
|
<script type="py" worker="./worker.py" config="./config.json"></script>
|
||||||
|
|
||||||
|
<!-- this is only to test the non-blocking behavior -->
|
||||||
<script>
|
<script>
|
||||||
// this is only to test non-blocking nature/bootstrap
|
|
||||||
addEventListener('DOMContentLoaded', () => {
|
addEventListener('DOMContentLoaded', () => {
|
||||||
const div = document.body.appendChild(
|
const div = document.body.appendChild(
|
||||||
document.createElement('div')
|
document.createElement('div')
|
||||||
@@ -31,4 +31,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="test"></div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from pyscript import document
|
from pyscript import display
|
||||||
|
|
||||||
import a
|
import a
|
||||||
|
|
||||||
document.body.append("Hello World")
|
display("Hello World", target="test", append=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user