From 97aa4513f7a70db077086ac0e3234a04e231328a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 20 Apr 2022 14:56:18 +0200 Subject: [PATCH] Add MIME renderer support --- pyscriptjs/src/interpreter.ts | 102 ++++++++++++++++++++++++++++----- pyscriptjs/src/pyscript.py | 105 +++++++++++++++++++++++++++++----- 2 files changed, 178 insertions(+), 29 deletions(-) diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 48ddd62..d3c1aed 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -14,6 +14,87 @@ 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'{mime};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. + """ + format_dict, md_dict = eval_formatter(obj, '_repr_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 @@ -24,23 +105,18 @@ class PyScript: 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); - if hasattr(value, "savefig"): - console.log(f"FIGURE: {value}") - buf = io.BytesIO() - value.savefig(buf, format='png') - buf.seek(0) - img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8') - document.getElementById(element_id).innerHTML = f'
' - elif hasattr(value, "startswith") and value.startswith("data:image"): - document.getElementById(element_id).innerHTML = f'
' + element = document.getElementById(element_id) + html, mime_type = format_mime(value) + console.log(mime_type, html) + if mime_type == 'application/javascript': + scriptEl = document.createRange().createContextualFragment(html) + element.children = [scriptEl] else: - document.getElementById(element_id).innerHTML = value; + element.innerHTML = html @staticmethod def run_until_complete(f): @@ -109,7 +185,6 @@ class Element: return Element(clone.id, clone) - def remove_class(self, classname): if isinstance(classname, list): for cl in classname: @@ -277,7 +352,6 @@ class PyListTemplate: """Overwrite me to define logic""" pass - class OutputCtxManager: def __init__(self, out=None, output_to_console=True, append=True): diff --git a/pyscriptjs/src/pyscript.py b/pyscriptjs/src/pyscript.py index 81fb12d..06a61d9 100644 --- a/pyscriptjs/src/pyscript.py +++ b/pyscriptjs/src/pyscript.py @@ -1,9 +1,91 @@ from js import document, setInterval, console +import micropip import asyncio -import io, base64 +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'{mime};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. + """ + format_dict, md_dict = eval_formatter(obj, '_repr_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 @@ -18,19 +100,14 @@ class PyScript: element_id = child.id = f"{element_id}-{exec_id}"; element.appendChild(child); - if hasattr(value, "savefig"): - console.log(f"FIGURE: {value}") - buf = io.BytesIO() - value.savefig(buf, format='png') - buf.seek(0) - img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8') - document.getElementById(element_id).innerHTML = f'
' - elif hasattr(value, "startswith") and value.startswith("data:image"): - console.log(f"DATA/IMAGE: {value}") - document.getElementById(element_id).innerHTML = f'
' + element = document.getElementById(element_id) + html, mime_type = format_mime(value) + console.log(mime_type, html) + if mime_type == 'application/javascript': + scriptEl = document.createRange().createContextualFragment(html) + element.children = [scriptEl] else: - document.getElementById(element_id).innerHTML = repr(value); - console.log(f"ELSE: {append} ==> {element_id} --> {value}") + element.innerHTML = html @staticmethod def run_until_complete(f): @@ -86,5 +163,3 @@ class Element: self.element.after(clone); return Element(clone.id, clone) - -pyscript = PyScript()