Files
pyscript/core/src/stdlib/pyscript/display.py
Andrea Giammarchi 4937a46731 Updated Polyscript to its latest (#2364)
* Fix #2360 - Better shared env/setup handling (#2361)

* Updated Polyscript to its latest

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changed is_null to a more Pythonic is_none

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-06 19:23:24 +02:00

180 lines
5.3 KiB
Python

import base64
import html
import io
import re
from pyscript.magic_js import current_target, document, window
from pyscript.ffi import is_none
_MIME_METHODS = {
"savefig": "image/png",
"_repr_javascript_": "application/javascript",
"_repr_json_": "application/json",
"_repr_latex": "text/latex",
"_repr_png_": "image/png",
"_repr_jpeg_": "image/jpeg",
"_repr_pdf_": "application/pdf",
"_repr_svg_": "image/svg+xml",
"_repr_markdown_": "text/markdown",
"_repr_html_": "text/html",
"__repr__": "text/plain",
}
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>",
}
class HTML:
"""
Wrap a string so that display() can render it as plain HTML
"""
def __init__(self, html):
self._html = html
def _repr_html_(self):
return self._html
def _eval_formatter(obj, print_method):
"""
Evaluates a formatter method.
"""
if print_method == "__repr__":
return repr(obj)
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 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 _MIME_METHODS.items():
if mime_type in format_dict:
output = format_dict[mime_type]
else:
output = _eval_formatter(obj, method)
if is_none(output):
continue
if mime_type not in _MIME_RENDERERS:
not_available.append(mime_type)
continue
break
if is_none(output):
if not_available:
window.console.warn(
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")
element.append(out_element)
else:
out_element = element.lastElementChild
if is_none(out_element):
out_element = element
if mime_type in ("application/javascript", "text/html"):
script_element = document.createRange().createContextualFragment(html)
out_element.append(script_element)
else:
out_element.innerHTML = html
def display(*values, target=None, append=True):
if is_none(target):
target = current_target()
elif not isinstance(target, str):
msg = f"target must be str or None, not {target.__class__.__name__}"
raise TypeError(msg)
elif target == "":
msg = "Cannot have an empty target"
raise ValueError(msg)
elif target.startswith("#"):
# note: here target is str and not None!
# align with @when behavior
target = target[1:]
element = document.getElementById(target)
# If target cannot be found on the page, a ValueError is raised
if is_none(element):
msg = f"Invalid selector with id={target}. Cannot be found in the page."
raise ValueError(msg)
# if element is a <script type="py">, it has a 'target' attribute which
# points to the visual element holding the displayed values. In that case,
# use that.
if element.tagName == "SCRIPT" and hasattr(element, "target"):
element = element.target
for v in values:
if not append:
element.replaceChildren()
_write(element, v, append=append)