mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Compare commits
11 Commits
2024.5.1
...
fpliger/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e8c00f79 | ||
|
|
b1aa4d345b | ||
|
|
6f516ed4d1 | ||
|
|
04d117c12d | ||
|
|
579e7ab87a | ||
|
|
10e497c753 | ||
|
|
3b46609614 | ||
|
|
320ca306bd | ||
|
|
e087deef09 | ||
|
|
c3bac976c8 | ||
|
|
4c3e5fabb9 |
File diff suppressed because one or more lines are too long
@@ -20,5 +20,6 @@ export default {
|
|||||||
output: {
|
output: {
|
||||||
esModule: true,
|
esModule: true,
|
||||||
file: "./core.js",
|
file: "./core.js",
|
||||||
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ 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";
|
||||||
@@ -71,6 +68,18 @@ const bootstrapNodeAndPlugins = (pyodide, element, callback, hook) => {
|
|||||||
for (const fn of hooks[hook]) fn(pyodide, element);
|
for (const fn of hooks[hook]) fn(pyodide, element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// these are imported as string (via rollup)
|
||||||
|
import init_py from "./stdlib/_pyscript/__init__.py";
|
||||||
|
import display_py from "./stdlib/_pyscript/_display.py";
|
||||||
|
|
||||||
|
const writeStdlib = (pyodide, element) => {
|
||||||
|
console.log("writeStdlib!");
|
||||||
|
const FS = pyodide.interpreter.FS;
|
||||||
|
FS.mkdirTree("/home/pyodide/_pyscript");
|
||||||
|
FS.writeFile("_pyscript/__init__.py", init_py, { encoding: "utf8" });
|
||||||
|
FS.writeFile("_pyscript/_display.py", display_py, { encoding: "utf8" });
|
||||||
|
};
|
||||||
|
|
||||||
const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
||||||
// automatically use the pyscript stderr (when/if defined)
|
// automatically use the pyscript stderr (when/if defined)
|
||||||
// this defaults to console.error
|
// this defaults to console.error
|
||||||
@@ -81,15 +90,20 @@ const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
|
|||||||
}
|
}
|
||||||
// trap once the python `display` utility (borrowed from "classic PyScript")
|
// trap once the python `display` utility (borrowed from "classic PyScript")
|
||||||
// provide the regular Pyodide globals instead of those from xworker
|
// provide the regular Pyodide globals instead of those from xworker
|
||||||
const pyDisplay = interpreter.runPython(
|
// const pyDisplay = interpreter.runPython(
|
||||||
[
|
// [
|
||||||
"import js",
|
// "import js",
|
||||||
"document=js.document",
|
// "document=js.document",
|
||||||
"window=js",
|
// "window=js",
|
||||||
display,
|
// display,
|
||||||
"display",
|
// "display",
|
||||||
].join("\n"),
|
// ].join("\n"),
|
||||||
);
|
// );
|
||||||
|
const pyDisplay = interpreter.runPython(`
|
||||||
|
from _pyscript import display
|
||||||
|
display
|
||||||
|
`);
|
||||||
|
|
||||||
interpreter.registerJsModule("pyscript", {
|
interpreter.registerJsModule("pyscript", {
|
||||||
PyWorker,
|
PyWorker,
|
||||||
document,
|
document,
|
||||||
@@ -139,6 +153,8 @@ export const hooks = {
|
|||||||
codeAfterRunWorkerAsync: new Set(),
|
codeAfterRunWorkerAsync: new Set(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// XXX antocuni: I think this is broken, because now _display.py imports
|
||||||
|
// window and document directly from js
|
||||||
const workerPyScriptModule = [
|
const workerPyScriptModule = [
|
||||||
"from pyodide_js import FS",
|
"from pyodide_js import FS",
|
||||||
`FS.writeFile('./pyscript.py', ${JSON.stringify(
|
`FS.writeFile('./pyscript.py', ${JSON.stringify(
|
||||||
@@ -147,7 +163,7 @@ const workerPyScriptModule = [
|
|||||||
"document=polyscript.xworker.window.document",
|
"document=polyscript.xworker.window.document",
|
||||||
"window=polyscript.xworker.window",
|
"window=polyscript.xworker.window",
|
||||||
"sync=polyscript.xworker.sync",
|
"sync=polyscript.xworker.sync",
|
||||||
display,
|
display_py,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
)})`,
|
)})`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
@@ -183,6 +199,9 @@ define("py", {
|
|||||||
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
||||||
},
|
},
|
||||||
async onInterpreterReady(pyodide, element) {
|
async onInterpreterReady(pyodide, element) {
|
||||||
|
console.log("onInterpreterReady");
|
||||||
|
writeStdlib(pyodide, element);
|
||||||
|
console.log("after writeStdlib");
|
||||||
registerModule(pyodide, element);
|
registerModule(pyodide, element);
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
# ⚠️ WARNING - both `document` and `window` are added at runtime
|
# ⚠️ WARNING - both `document` and `window` are added at runtime
|
||||||
|
|
||||||
|
# XXX antocuni: I think this is wrong: it works in the main thread but not in
|
||||||
|
# the worker, because the rest of the code expects window and document to be
|
||||||
|
# proxies (see workerPyScriptModule in core.js)
|
||||||
import base64
|
import base64
|
||||||
import html
|
import html
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from js import document, window
|
||||||
|
|
||||||
_MIME_METHODS = {
|
_MIME_METHODS = {
|
||||||
"__repr__": "text/plain",
|
"__repr__": "text/plain",
|
||||||
@@ -102,7 +106,7 @@ def _format_mime(obj):
|
|||||||
break
|
break
|
||||||
if output is None:
|
if output is None:
|
||||||
if not_available:
|
if not_available:
|
||||||
window.console.warn(
|
window.console.warn( # noqa: F821
|
||||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||||
)
|
)
|
||||||
output = repr(output)
|
output = repr(output)
|
||||||
@@ -120,7 +124,7 @@ def _write(element, value, append=False):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if append:
|
if append:
|
||||||
out_element = document.createElement("div")
|
out_element = document.createElement("div") # noqa: F821
|
||||||
element.append(out_element)
|
element.append(out_element)
|
||||||
else:
|
else:
|
||||||
out_element = element.lastElementChild
|
out_element = element.lastElementChild
|
||||||
@@ -128,13 +132,15 @@ def _write(element, value, append=False):
|
|||||||
out_element = element
|
out_element = element
|
||||||
|
|
||||||
if mime_type in ("application/javascript", "text/html"):
|
if mime_type in ("application/javascript", "text/html"):
|
||||||
script_element = document.createRange().createContextualFragment(html)
|
script_element = document.createRange().createContextualFragment( # noqa: F821
|
||||||
|
html
|
||||||
|
)
|
||||||
out_element.append(script_element)
|
out_element.append(script_element)
|
||||||
else:
|
else:
|
||||||
out_element.innerHTML = html
|
out_element.innerHTML = html
|
||||||
|
|
||||||
|
|
||||||
def display(*values, target=None, append=True):
|
def display(*values, target=None, append=True):
|
||||||
element = document.getElementById(target)
|
element = document.getElementById(target) # noqa: F821
|
||||||
for v in values:
|
for v in values:
|
||||||
_write(element, v, append=append)
|
_write(element, v, append=append)
|
||||||
354
pyscript.core/src/python/pyweb/pydom.py
Normal file
354
pyscript.core/src/python/pyweb/pydom.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
from functools import cached_property
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import js
|
||||||
|
|
||||||
|
# from js import document as js_document
|
||||||
|
from pyodide.ffi import JsProxy
|
||||||
|
from pyodide.ffi.wrappers import add_event_listener
|
||||||
|
from pyscript import display
|
||||||
|
|
||||||
|
alert = js.alert
|
||||||
|
|
||||||
|
|
||||||
|
class BaseElement:
|
||||||
|
def __init__(self, js_element):
|
||||||
|
self._element = js_element
|
||||||
|
self._parent = None
|
||||||
|
self.style = StyleProxy(self)
|
||||||
|
|
||||||
|
def __eq__(self, obj):
|
||||||
|
"""Check if the element is the same as the other element by comparing
|
||||||
|
the underlying JS element"""
|
||||||
|
return isinstance(obj, BaseElement) and obj._element == self._element
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
if self._parent:
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
if self._element.parentElement:
|
||||||
|
self._parent = self.__class__(self._element.parentElement)
|
||||||
|
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __class(self):
|
||||||
|
return self.__class__ if self.__class__ != PyDom else Element
|
||||||
|
|
||||||
|
def create(self, type_, is_child=True, classes=None, html=None, label=None):
|
||||||
|
js_el = js.document.createElement(type_)
|
||||||
|
element = self.__class(js_el)
|
||||||
|
|
||||||
|
if classes:
|
||||||
|
for class_ in classes:
|
||||||
|
element.add_class(class_)
|
||||||
|
|
||||||
|
if html is not None:
|
||||||
|
element.html = html
|
||||||
|
|
||||||
|
if label is not None:
|
||||||
|
element.label = label
|
||||||
|
|
||||||
|
if is_child:
|
||||||
|
self.append(element)
|
||||||
|
|
||||||
|
return element
|
||||||
|
|
||||||
|
|
||||||
|
class Element(BaseElement):
|
||||||
|
def append(self, child):
|
||||||
|
# TODO: this is Pyodide specific for now!!!!!!
|
||||||
|
# if we get passed a JSProxy Element directly we just map it to the
|
||||||
|
# higher level Python element
|
||||||
|
if isinstance(child, JsProxy):
|
||||||
|
return self.append(self.from_js(child))
|
||||||
|
|
||||||
|
elif isinstance(child, Element):
|
||||||
|
self._element.appendChild(child._element)
|
||||||
|
|
||||||
|
return child
|
||||||
|
|
||||||
|
def from_js(self, js_element):
|
||||||
|
return self.__class__(js.tagName, parent=self)
|
||||||
|
|
||||||
|
# TODO: These 2 should be changed to basically do what PyDom.__getitem__ does
|
||||||
|
# but within the scope of the current element children
|
||||||
|
def query(self, selector):
|
||||||
|
"""The querySelector() method of the Element interface returns the first element
|
||||||
|
that is a descendant of the element on which it is invoked that matches the specified
|
||||||
|
group of selectors.
|
||||||
|
"""
|
||||||
|
return self.__class__(self._element.querySelector(selector))
|
||||||
|
|
||||||
|
def query_all(self, selector):
|
||||||
|
"""The querySelectorAll() method of the Element interface returns a static (not live)
|
||||||
|
NodeList representing a list of the document's elements that match the specified group
|
||||||
|
of selectors.
|
||||||
|
"""
|
||||||
|
for element in self._element.querySelectorAll(selector):
|
||||||
|
yield self.__class__(element)
|
||||||
|
|
||||||
|
# -------- Boilerplate Proxy for the Element API -------- #
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._element.innerHTML
|
||||||
|
|
||||||
|
@html.setter
|
||||||
|
def html(self, value):
|
||||||
|
self._element.innerHTML = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self):
|
||||||
|
return self._element.innerHTML
|
||||||
|
|
||||||
|
@content.setter
|
||||||
|
def content(self, value):
|
||||||
|
display(value, target=self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self._element.id
|
||||||
|
|
||||||
|
@id.setter
|
||||||
|
def id(self, value):
|
||||||
|
self._element.id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checked(self):
|
||||||
|
return self._element.checked
|
||||||
|
|
||||||
|
@checked.setter
|
||||||
|
def checked(self, value):
|
||||||
|
self._element.checked = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
tag = self._element.tagName
|
||||||
|
if tag == "INPUT":
|
||||||
|
if self._element.type == "checkbox":
|
||||||
|
return self._element.checked
|
||||||
|
elif self._element.type == "number":
|
||||||
|
return float(self._element.value)
|
||||||
|
else:
|
||||||
|
return self._element.value
|
||||||
|
return self._element.innerHTML
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
# TODO: This needs a bit more thinking. SHould we set .innerHTML or .text for instance?
|
||||||
|
tag = self._element.tagName
|
||||||
|
# print(f"Writing ({tag} )---> {self._selector} ---> {value}")
|
||||||
|
if tag == "INPUT":
|
||||||
|
# print(f"Writing ({tag} | {self._element.type})---> {self._selector} ---> {value}")
|
||||||
|
if self._element.type == "checkbox":
|
||||||
|
self._element.checked = value
|
||||||
|
elif self._element.type == "number":
|
||||||
|
self._element.value = float(value)
|
||||||
|
else:
|
||||||
|
self._element.value = value
|
||||||
|
else:
|
||||||
|
self._element.innerHTML = value
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.value = ""
|
||||||
|
|
||||||
|
def clone(self, new_id=None):
|
||||||
|
clone = Element(self._element.cloneNode(True))
|
||||||
|
clone.id = new_id
|
||||||
|
|
||||||
|
return clone
|
||||||
|
|
||||||
|
def remove_class(self, classname):
|
||||||
|
classList = self._element.classList
|
||||||
|
if isinstance(classname, list):
|
||||||
|
classList.remove(*classname)
|
||||||
|
else:
|
||||||
|
classList.remove(classname)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_class(self, classname):
|
||||||
|
classList = self._element.classList
|
||||||
|
if isinstance(classname, list):
|
||||||
|
classList.add(*classname)
|
||||||
|
else:
|
||||||
|
self._element.classList.add(classname)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def classes(self):
|
||||||
|
classes = self._element.classList.values()
|
||||||
|
return [x for x in classes]
|
||||||
|
|
||||||
|
def show_me(self):
|
||||||
|
self._element.scrollIntoView()
|
||||||
|
|
||||||
|
def when(self, event, handler):
|
||||||
|
document.when(self, event)(handler)
|
||||||
|
|
||||||
|
|
||||||
|
class StyleProxy(dict):
|
||||||
|
def __init__(self, element: Element) -> None:
|
||||||
|
self._element = element
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _style(self):
|
||||||
|
return self._element._element.style
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
self._style[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._style.setProperty(key, value)
|
||||||
|
|
||||||
|
def pop(self, key):
|
||||||
|
self._style.removeProperty(key)
|
||||||
|
|
||||||
|
def set(self, **kws):
|
||||||
|
for k, v in kws.items():
|
||||||
|
self._element._element.style.setProperty(k, v)
|
||||||
|
|
||||||
|
# CSS Properties
|
||||||
|
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
|
||||||
|
@property
|
||||||
|
def visibility(self):
|
||||||
|
return self._element._element.style.visibility
|
||||||
|
|
||||||
|
@visibility.setter
|
||||||
|
def visibility(self, value):
|
||||||
|
self._element._element.style.visibility = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def background(self):
|
||||||
|
return self._element._element.style.background
|
||||||
|
|
||||||
|
@background.setter
|
||||||
|
def background(self, value):
|
||||||
|
self._element._element.style.background = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
return self._element._element.style.color
|
||||||
|
|
||||||
|
@color.setter
|
||||||
|
def color(self, value):
|
||||||
|
self._element._element.style.color = value
|
||||||
|
|
||||||
|
|
||||||
|
class StyleCollection:
|
||||||
|
def __init__(self, collection: "ElementCollection") -> None:
|
||||||
|
self._collection = collection
|
||||||
|
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
return obj._get_attribute("style")
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._collection._get_attribute("style")[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
for element in self._collection._elements:
|
||||||
|
element.style[key] = value
|
||||||
|
|
||||||
|
def pop(self, key):
|
||||||
|
for element in self._collection._elements:
|
||||||
|
element.style.pop(key)
|
||||||
|
|
||||||
|
|
||||||
|
class ElementCollection:
|
||||||
|
def __init__(self, elements: [Element]) -> None:
|
||||||
|
self._elements = elements
|
||||||
|
self.style = StyleCollection(self)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, int):
|
||||||
|
return self._elements[key]
|
||||||
|
elif isinstance(key, slice):
|
||||||
|
return ElementCollection(self._elements[key])
|
||||||
|
|
||||||
|
# TODO: In this case what do we expect??
|
||||||
|
elements = self._element.querySelectorAll(key)
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
def _get_attribute(self, attr):
|
||||||
|
# As JQuery, when getting an attr, only return it for the first element
|
||||||
|
return getattr(self._elements[0], attr)
|
||||||
|
|
||||||
|
def _set_attribute(self, attr, value):
|
||||||
|
for el in self._elements:
|
||||||
|
setattr(el, attr, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._get_attribute("html")
|
||||||
|
|
||||||
|
@html.setter
|
||||||
|
def html(self, value):
|
||||||
|
self._set_attribute("html", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return self._elements
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self._elements
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
||||||
|
|
||||||
|
|
||||||
|
class DomScope:
|
||||||
|
def __getattr__(self, __name: str) -> Any:
|
||||||
|
element = document[f"#{__name}"]
|
||||||
|
if element:
|
||||||
|
return element[0]
|
||||||
|
|
||||||
|
|
||||||
|
class PyDom(BaseElement):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(js.document)
|
||||||
|
self.ids = DomScope()
|
||||||
|
|
||||||
|
def create(self, type_, parent=None, classes=None, html=None):
|
||||||
|
return super().create(type_, is_child=False)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if isinstance(key, int):
|
||||||
|
indices = range(*key.indices(len(self.list)))
|
||||||
|
return [self.list[i] for i in indices]
|
||||||
|
|
||||||
|
elements = self._element.querySelectorAll(key)
|
||||||
|
if not elements:
|
||||||
|
return None
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def when(element, event_type):
|
||||||
|
# TODO: Ideally, we should have that implemented in PyScript not patched here
|
||||||
|
# if isinstance(element, Element):
|
||||||
|
# element = [element]
|
||||||
|
def decorator(func):
|
||||||
|
# elements = js.document.querySelectorAll(selector)
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
|
||||||
|
# Function doesn't receive events
|
||||||
|
if not sig.parameters:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
func()
|
||||||
|
|
||||||
|
# for el in element:
|
||||||
|
add_event_listener(element._element, event_type, wrapper)
|
||||||
|
else:
|
||||||
|
# for el in element:
|
||||||
|
add_event_listener(element._element, event_type, func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
document = PyDom()
|
||||||
|
|
||||||
|
|
||||||
|
sys.modules[__name__] = document
|
||||||
0
pyscript.core/src/stdlib/_pyscript/__init__.py
Normal file
0
pyscript.core/src/stdlib/_pyscript/__init__.py
Normal file
146
pyscript.core/src/stdlib/_pyscript/_display.py
Normal file
146
pyscript.core/src/stdlib/_pyscript/_display.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# ⚠️ WARNING - both `document` and `window` are added at runtime
|
||||||
|
|
||||||
|
# XXX antocuni: I think this is wrong: it works in the main thread but not in
|
||||||
|
# the worker, because the rest of the code expects window and document to be
|
||||||
|
# proxies (see workerPyScriptModule in core.js)
|
||||||
|
import base64
|
||||||
|
import html
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
|
from js import document, window
|
||||||
|
|
||||||
|
_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( # noqa: F821
|
||||||
|
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):
|
||||||
|
html, mime_type = _format_mime(value)
|
||||||
|
if html == "\\n":
|
||||||
|
return
|
||||||
|
|
||||||
|
if append:
|
||||||
|
out_element = document.createElement("div") # noqa: F821
|
||||||
|
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( # noqa: F821
|
||||||
|
html
|
||||||
|
)
|
||||||
|
out_element.append(script_element)
|
||||||
|
else:
|
||||||
|
out_element.innerHTML = html
|
||||||
|
|
||||||
|
|
||||||
|
def display(*values, target=None, append=True):
|
||||||
|
element = document.getElementById(target) # noqa: F821
|
||||||
|
for v in values:
|
||||||
|
_write(element, v, append=append)
|
||||||
Reference in New Issue
Block a user