From 14a957c85f34b3af93caa74211b12a5fa7918d22 Mon Sep 17 00:00:00 2001 From: marvinscham Date: Fri, 29 Jul 2022 20:37:23 +0000 Subject: [PATCH] Update assets --- assets/pyscript.py | 424 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 assets/pyscript.py diff --git a/assets/pyscript.py b/assets/pyscript.py new file mode 100644 index 0000000..b57e502 --- /dev/null +++ b/assets/pyscript.py @@ -0,0 +1,424 @@ +import asyncio +import base64 +import io +import sys +import time +from textwrap import dedent + +import micropip # noqa: F401 +from js import console, document + +loop = asyncio.get_event_loop() + +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): + data = f"data:{mime};charset=utf-8;base64,{value}" + attrs = " ".join(['{k}="{v}"' for k, v in meta.items()]) + return f'' + + +def identity(value, meta): + return value + + +MIME_RENDERERS = { + "text/plain": identity, + "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"", +} + + +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 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: + console.warning( + 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 + + +class PyScript: + loop = loop + + @staticmethod + def write(element_id, value, append=False, exec_id=0): + """Writes value to the element with id "element_id""" + console.log(f"APPENDING: {append} ==> {element_id} --> {value}") + if append: + child = document.createElement("div") + element = document.querySelector(f"#{element_id}") + if not element: + return + exec_id = exec_id or element.childElementCount + 1 + element_id = child.id = f"{element_id}-{exec_id}" + element.appendChild(child) + + element = document.getElementById(element_id) + html, mime_type = format_mime(value) + if mime_type in ("application/javascript", "text/html"): + script_element = document.createRange().createContextualFragment(html) + element.appendChild(script_element) + else: + element.innerHTML = html + + @staticmethod + def run_until_complete(f): + _ = loop.run_until_complete(f) + + +class Element: + def __init__(self, element_id, element=None): + self._id = element_id + self._element = element + + @property + def id(self): + return self._id + + @property + def element(self): + """Return the dom element""" + if not self._element: + self._element = document.querySelector(f"#{self._id}") + return self._element + + @property + def value(self): + return self.element.value + + @property + def innerHtml(self): + return self.element.innerHtml + + def write(self, value, append=False): + console.log(f"Element.write: {value} --> {append}") + # TODO: it should be the opposite... pyscript.write should use the Element.write + # so we can consolidate on how we write depending on the element type + pyscript.write(self._id, value, append=append) + + def clear(self): + if hasattr(self.element, "value"): + self.element.value = "" + else: + self.write("", append=False) + + def select(self, query, from_content=False): + el = self.element + if from_content: + el = el.content + + _el = el.querySelector(query) + if _el: + return Element(_el.id, _el) + else: + console.log(f"WARNING: can't find element matching query {query}") + + def clone(self, new_id=None, to=None): + if new_id is None: + new_id = self.element.id + + clone = self.element.cloneNode(True) + clone.id = new_id + + if to: + to.element.appendChild(clone) + + # Inject it into the DOM + self.element.after(clone) + + return Element(clone.id, clone) + + def remove_class(self, classname): + if isinstance(classname, list): + for cl in classname: + self.remove_class(cl) + else: + self.element.classList.remove(classname) + + def add_class(self, classname): + self.element.classList.add(classname) + + +def add_classes(element, class_list): + for klass in class_list.split(" "): + element.classList.add(klass) + + +def create(what, id_=None, classes=""): + element = document.createElement(what) + if id_: + element.id = id_ + add_classes(element, classes) + return Element(id_, element) + + +class PyWidgetTheme: + def __init__(self, main_style_classes): + self.main_style_classes = main_style_classes + + def theme_it(self, widget): + for klass in self.main_style_classes.split(" "): + widget.classList.add(klass) + + +class PyItemTemplate(Element): + label_fields = None + + def __init__(self, data, labels=None, state_key=None, parent=None): + self.data = data + + self.register_parent(parent) + + if not labels: + labels = list(self.data.keys()) + self.labels = labels + + self.state_key = state_key + + super().__init__(self._id) + + def register_parent(self, parent): + self._parent = parent + if parent: + self._id = f"{self._parent._id}-c-{len(self._parent._children)}" + self.data["id"] = self._id + else: + self._id = None + + def create(self): + console.log("creating section") + new_child = create("section", self._id, "task bg-white my-1") + console.log("creating values") + + console.log("creating innerHtml") + new_child._element.innerHTML = dedent( + f""" + + """ + ) + + console.log("returning") + return new_child + + def on_click(self, evt): + pass + + def pre_append(self): + pass + + def post_append(self): + self.element.click = self.on_click + self.element.onclick = self.on_click + + self._post_append() + + def _post_append(self): + pass + + def strike(self, value, extra=None): + if value: + self.add_class("line-through") + else: + self.remove_class("line-through") + + def render_content(self): + return " - ".join([self.data[f] for f in self.labels]) + + +class PyListTemplate: + theme = PyWidgetTheme("flex flex-col-reverse mt-8 mx-8") + item_class = PyItemTemplate + + def __init__(self, parent): + self.parent = parent + self._children = [] + self._id = self.parent.id + + @property + def children(self): + return self._children + + @property + def data(self): + return [c.data for c in self._children] + + def render_children(self): + binds = {} + for i, c in enumerate(self._children): + txt = c.element.innerHTML + rnd = str(time.time()).replace(".", "")[-5:] + new_id = f"{c.element.id}-{i}-{rnd}" + binds[new_id] = c.element.id + txt = txt.replace(">", f" id='{new_id}'>") + print(txt) + + def foo(evt): + console.log(evt) + evtEl = evt.srcElement + srcEl = Element(binds[evtEl.id]) + srcEl.element.onclick() + evtEl.classList = srcEl.element.classList + + for new_id in binds: + Element(new_id).element.onclick = foo + + def connect(self): + self.md = main_div = document.createElement("div") + main_div.id = self._id + "-list-tasks-container" + + if self.theme: + self.theme.theme_it(main_div) + + self.parent.appendChild(main_div) + + def add(self, *args, **kws): + if not isinstance(args[0], self.item_class): + child = self.item_class(*args, **kws) + else: + child = args[0] + child.register_parent(self) + return self._add(child) + + def _add(self, child_elem): + console.log("appending child", child_elem.element) + self.pre_child_append(child_elem) + child_elem.pre_append() + self._children.append(child_elem) + self.md.appendChild(child_elem.create().element) + child_elem.post_append() + self.child_appended(child_elem) + return child_elem + + def pre_child_append(self, child): + pass + + def child_appended(self, child): + """Overwrite me to define logic""" + pass + + +class OutputCtxManager: + def __init__(self, out=None, output_to_console=True, append=True): + self._out = out + self._prev = out + self.output_to_console = output_to_console + self._append = append + + def change(self, out=None, err=None, output_to_console=True, append=True): + self._prev = self._out + self._out = out + self.output_to_console = output_to_console + self._append = append + console.log("----> changed out to", self._out, self._append) + + def revert(self): + console.log("----> reverted") + self._out = self._prev + + def write(self, txt): + console.log("writing to", self._out, txt, self._append) + if self._out: + pyscript.write(self._out, txt, append=self._append) + if self.output_to_console: + console.log(self._out, txt) + + +class OutputManager: + def __init__(self, out=None, err=None, output_to_console=True, append=True): + sys.stdout = self._out_manager = OutputCtxManager( + out, output_to_console, append + ) + sys.stderr = self._err_manager = OutputCtxManager( + err, output_to_console, append + ) + self.output_to_console = output_to_console + self._append = append + + def change(self, out=None, err=None, output_to_console=True, append=True): + self._out_manager.change(out, output_to_console, append) + sys.stdout = self._out_manager + self._err_manager.change(err, output_to_console, append) + sys.stderr = self._err_manager + self.output_to_console = output_to_console + self.append = append + + def revert(self): + self._out_manager.revert() + self._err_manager.revert() + sys.stdout = self._out_manager + sys.stderr = self._err_manager + console.log("----> reverted") + + +pyscript = PyScript() +output_manager = OutputManager()