rendering a notebook with pyscript
¤
let's convert a notebook to a pyscript document.
we'll using nbconvert and jinja2
to transform a notebook into a standalone interactive document.
jinja2
template overrides for nbconvert
¤
our pyscript extends the base jupyterlab styling.
template = """
{%- extends 'lab/index.html.j2' -%}"""
include pyscript css and javascript assets in the <head>
template += """
{%- block header -%}
{{super()}}
<link href="https://pyscript.net/latest/pyscript.css" rel="stylesheet"/>
<script defer="" src="https://pyscript.net/latest/pyscript.js"></script>
{%- endblock header -%}"""
requirejs and pyscript do not play well together, and the block below prevents requirejs from loading.
template += """
{%- block html_head_js -%}
{%- endblock html_head_js -%}"""
from a notebook's source we can infer the packages it needs to run.
the packages are placed in the <py-config>
tag.
> continue reading to see how the get_imports_from_cells
filter is defined.
template += """
{% block body_header %}
{{super()}}
<py-config>
packages = {{nb | get_imports_from_cells}}
</py-config>
{% endblock body_header %}"""
we'll replace standard pygments cell inputs with a <py-repl>
with autogenerate disabled.
template += """
{% block input %}
<py-repl output="out-{{cell.id}}">
{{cell.source | escape | dedent}}
</py-repl>
{% endblock input %}"""
we use any existing outputs as the dead pixels. the outputs are replaced the first pyscript executes in a nearby cell.
template += """
{% block output %}
<div id="out-{{cell.id}}">{{super()}}</div>
{% endblock output %}
{% block codecell %}
{{super()}}
{% if not cell.outputs %}
<div id="out-{{cell.id}}"></div>
{% endif %}
{% endblock codecell %}
"""
our template
is defined for<py-script>
documents. next we introduce template
into the nbconvert
machinery.
nbconvert
exporting machinery¤
nbconvert
is the primary machinery used to transform notebook documents into other file formats. it is a wrapper around the shape of the notebook and a jinja2
environment.
import depfinder; from pathlib import Path; from functools import partial
from nbconvert.exporters import HTMLExporter, TemplateExporter
inferring dependencies¤
the py-config
defines the environment. we use depfinder
to do that.
def get_imports_from_cells(nb):
imports = set()
for cell in nb.cells:
imports.update(get_imports_from_cell(cell))
if imports.intersection({"requests", "httpx", "urllib"}): # add more later
imports.add("pyodide-http")
return list(imports)
def get_imports_from_cell(cell):
import depfinder
__import__("requests_cache").install_cache()
if cell["cell_type"] == "code":
try:
yield from depfinder.inspection.get_imported_libs(textwrap.dedent("".join(cell["source"]))).required_modules
except BaseException as e:
pass
the nbconvert
exporter¤
get_exporter
generates a new notebook file converter.
- adds filters used in
template
- puts a template on the
jinja2.DictLoader
with our custom template
def get_exporter(template=template):
import textwrap, html, jinja2
exporter = HTMLExporter(
template_file="pyscript.j2", filters=dict(
dedent=textwrap.dedent, get_imports_from_cells=get_imports_from_cells, escape=html.escape
)
)
for loader in exporter.environment.loader.loaders:
if isinstance(loader, jinja2.DictLoader):
loader.mapping["pyscript.j2"] = template
return exporter
pyscript
transformation functions¤
get_pyscript
turns a file into a string of py-script html.
def get_pyscript(file): return get_exporter(template).from_filename(file)[0]
pyscript
transforms a file and writes the py-script document to disk.
def pyscript(file: Path, target: Path = None, write: bool=True):
"""generate a pyscript version of a notebook"""
body = get_pyscript(file)
if write:
if not target:
target = file.with_suffix(F"{file.suffix}.html")
target.write_text(body)
print(F"created {target}")
if __name__ == "__main__" and "__file__" not in locals():
!python -m tonyfast pyscript 2022-12-19-integrating-typer.ipynb
from IPython.display import display, IFrame
display(IFrame(*"2022-12-19-integrating-typer.ipynb.html 100% 600".split()))