Skip to content

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()))
created 2022-12-19-integrating-typer.ipynb.html