from js import document, setInterval, console import micropip import asyncio import io, base64, sys 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 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)() if 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, md_dict = mimebundle else: format_dict = mimebundle md_dict = {} 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'): scriptEl = document.createRange().createContextualFragment(html) element.appendChild(scriptEl) else: element.innerHTML = html @staticmethod def run_until_complete(f): p = loop.run_until_complete(f) class Element: def __init__(self, element_id, element=None): self._id = element_id self._element = element @property def element(self): """Return the dom element""" if not self._element: self._element = document.querySelector(f'#{self._id}'); return self._element 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)