mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
11 Commits
2024.8.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: {
|
||||
esModule: true,
|
||||
file: "./core.js",
|
||||
sourcemap: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,9 +2,6 @@ import "@ungap/with-resolvers";
|
||||
import { $ } from "basic-devtools";
|
||||
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
|
||||
// we should factor this utility out a part but this works anyway.
|
||||
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);
|
||||
};
|
||||
|
||||
// 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 }) => {
|
||||
// automatically use the pyscript stderr (when/if defined)
|
||||
// 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")
|
||||
// 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"),
|
||||
);
|
||||
// const pyDisplay = interpreter.runPython(
|
||||
// [
|
||||
// "import js",
|
||||
// "document=js.document",
|
||||
// "window=js",
|
||||
// display,
|
||||
// "display",
|
||||
// ].join("\n"),
|
||||
// );
|
||||
const pyDisplay = interpreter.runPython(`
|
||||
from _pyscript import display
|
||||
display
|
||||
`);
|
||||
|
||||
interpreter.registerJsModule("pyscript", {
|
||||
PyWorker,
|
||||
document,
|
||||
@@ -139,6 +153,8 @@ export const hooks = {
|
||||
codeAfterRunWorkerAsync: new Set(),
|
||||
};
|
||||
|
||||
// XXX antocuni: I think this is broken, because now _display.py imports
|
||||
// window and document directly from js
|
||||
const workerPyScriptModule = [
|
||||
"from pyodide_js import FS",
|
||||
`FS.writeFile('./pyscript.py', ${JSON.stringify(
|
||||
@@ -147,7 +163,7 @@ const workerPyScriptModule = [
|
||||
"document=polyscript.xworker.window.document",
|
||||
"window=polyscript.xworker.window",
|
||||
"sync=polyscript.xworker.sync",
|
||||
display,
|
||||
display_py,
|
||||
].join("\n"),
|
||||
)})`,
|
||||
].join("\n");
|
||||
@@ -183,6 +199,9 @@ define("py", {
|
||||
bootstrapNodeAndPlugins(pyodide, element, after, "onAfterRunAsync");
|
||||
},
|
||||
async onInterpreterReady(pyodide, element) {
|
||||
console.log("onInterpreterReady");
|
||||
writeStdlib(pyodide, element);
|
||||
console.log("after writeStdlib");
|
||||
registerModule(pyodide, element);
|
||||
// allows plugins to do whatever they want with the element
|
||||
// before regular stuff happens in here
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# ⚠️ 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",
|
||||
@@ -102,7 +106,7 @@ def _format_mime(obj):
|
||||
break
|
||||
if output is None:
|
||||
if not_available:
|
||||
window.console.warn(
|
||||
window.console.warn( # noqa: F821
|
||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||
)
|
||||
output = repr(output)
|
||||
@@ -120,7 +124,7 @@ def _write(element, value, append=False):
|
||||
return
|
||||
|
||||
if append:
|
||||
out_element = document.createElement("div")
|
||||
out_element = document.createElement("div") # noqa: F821
|
||||
element.append(out_element)
|
||||
else:
|
||||
out_element = element.lastElementChild
|
||||
@@ -128,13 +132,15 @@ def _write(element, value, append=False):
|
||||
out_element = element
|
||||
|
||||
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)
|
||||
else:
|
||||
out_element.innerHTML = html
|
||||
|
||||
|
||||
def display(*values, target=None, append=True):
|
||||
element = document.getElementById(target)
|
||||
element = document.getElementById(target) # noqa: F821
|
||||
for v in values:
|
||||
_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