skip to main content

@tonyfast s notebooks

site navigation
notebook summary
title
rendering a notebook with pyscript
description
let's convert a notebook to a pyscript document.
cells
30 total
13 code
state
executed in order
kernel
Python [conda env:root] *
language
python
name
conda-root-py
lines of code
103
outputs
2
table of contents
{"kernelspec": {"display_name": "Python [conda env:root] *", "language": "python", "name": "conda-root-py"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.13"}, "title": "rendering a notebook with pyscript", "description": "let's convert a notebook to a pyscript document."}
notebook toolbar
Activate
cell ordering
1

rendering a notebook with

2

let's convert a notebook to a pyscript document.

we'll using nbconvert and jinja2 to transform a notebook into a standalone interactive document.

3

jinja2 nbconvert

4

our pyscript extends the base jupyterlab styling.

5
    template = """
    {%- extends 'lab/index.html.j2' -%}"""
6

include pyscript css and javascript assets in the <head>

7
    template += """
    {%- block header -%}
    {{super()}}
    <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
    <script defer src="https://pyscript.net/latest/pyscript.js"></script>
    {%- endblock header -%}"""
8

requirejs and pyscript do not play well together , and the block below prevents requirejs from loading.

9
    template += """
    {%- block html_head_js -%}
    {%- endblock html_head_js -%}"""
10

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.

11
    template += """
    {% block body_header %}   
    {{super()}}
    <py-config>
    packages = {{nb | get_imports_from_cells}}
    </py-config>
    {% endblock body_header %}"""
12

we'll replace standard pygments cell inputs with a <py-repl> with autogenerate disabled.

13
    template += """
    {% block input %}
    <py-repl output="out-{{cell.id}}">
    {{cell.source | escape | dedent}}
    </py-repl>
    {% endblock input %}"""
14

we use any existing outputs as the dead pixels. the outputs are replaced the first pyscript executes in a nearby cell.

15
    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 %}
    """
16

our template is defined for <py-script> documents. next we introduce template into the nbconvert machinery.

17

nbconvert

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.

18
    import depfinder; from pathlib import Path; from functools import partial
    from nbconvert.exporters import HTMLExporter, TemplateExporter
19

inferring dependencies

the py-config defines the environment. we use depfinder to do that.

20
    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
21

the exporter

22

get_exporter generates a new notebook file converter.

  • adds filters used in template
  • puts a template on the jinja2.DictLoader with our custom template
23
    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
24

pyscript

25

get_pyscript turns a file into a string of py-script html.

26
    def get_pyscript(file): return get_exporter(template).from_filename(file)[0]
27

pyscript transforms a file and writes the py-script document to disk.

28
    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}")               
29
    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()))
2 outputs.
created 2022-12-19-integrating-typer.ipynb.html

30