Skip to content

Commit

Permalink
feat(template): allow kernel starting and notebook execution control …
Browse files Browse the repository at this point in the history
…for progressive rendering
  • Loading branch information
maartenbreddels committed May 17, 2019
1 parent 0a93105 commit 29f2f8a
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 39 deletions.
Expand Up @@ -16,12 +16,6 @@

<script src="{{resources.base_url}}voila/static/jquery.min.js"></script>

<script id="jupyter-config-data" type="application/json">
{
"baseUrl": "{{resources.base_url}}",
"kernelId": "{{resources.kernel_id}}"
}
</script>
{%- endblock html_head_js -%}


Expand Down
45 changes: 41 additions & 4 deletions share/jupyter/voila/template/default/nbconvert_templates/voila.tpl
Expand Up @@ -50,15 +50,52 @@ div#notebook-container{

<!-- Loading mathjax macro -->
{{ mathjax() }}

<!-- voila spinner -->
<style type="text/css">
#loading {
display: flex;
align-items: center;
justify-content: center;
height: 75vh;
color: #444;
font-family: sans-serif;
}
</style>
{%- endblock html_head_css -%}

{% block body %}

<body data-base-url="{{resources.base_url}}voila/">
<div tabindex="-1" id="notebook" class="border-box-sizing">
<div class="container" id="notebook-container">
{{ super() }}
</div>

<div id="loading">
<h2><i class="fa fa-spinner fa-spin" style="font-size:36px;"></i> Running {{nb_title}}...</i></h2>
</div>

{# from this point on, the kernel is started #}
{%- with kernel_id = kernel_start() -%}
<script id="jupyter-config-data" type="application/json">
{
"baseUrl": "{{resources.base_url}}",
"kernelId": "{{kernel_id}}"
}
</script>
{# from this point on, nb.cells contains output of the executed cells #}
{% do notebook_execute(nb, kernel_id) %}
<div tabindex="-1" id="notebook" class="border-box-sizing">
<div class="container" id="notebook-container">
{{ super() }}
</div>
</div>
{% endwith %}
<script type="text/javascript">
// remove the loading element
(function() {
var el = document.getElementById("loading")
el.parentNode.removeChild(el)
})()
</script>

</body>
{%- endblock body %}

78 changes: 49 additions & 29 deletions voila/handler.py
Expand Up @@ -48,21 +48,6 @@ def get(self, path=None):
else:
raise tornado.web.HTTPError(404, 'file not found')

# Fetch kernel name from the notebook metadata
kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)

# Launch kernel and execute notebook
cwd = os.path.dirname(notebook_path)
kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name, path=cwd))
km = self.kernel_manager.get_kernel(kernel_id)
result = executenb(notebook, km=km, cwd=cwd)

# render notebook to html
resources = {
'kernel_id': kernel_id,
'base_url': self.base_url,
'nbextensions': nbextensions
}

exporter = HTMLExporter(
template_file='voila.tpl',
Expand All @@ -71,22 +56,57 @@ def get(self, path=None):
contents_manager=self.contents_manager # for the image inlining
)

if self.strip_sources:
exporter.exclude_input = True
exporter.exclude_output_prompt = True
exporter.exclude_input_prompt = True
resources = {
# 'kernel_id': kernel_id,
'base_url': self.base_url,
'nbextensions': nbextensions
}

# Filtering out empty cells.
def filter_empty_code_cells(cell):
return (
cell.cell_type != 'code' or # keep non-code cells
(cell.outputs and not exporter.exclude_output) # keep cell if output not excluded and not empty
or not exporter.exclude_input # keep cell if input not excluded
)
result.cells = list(filter(filter_empty_code_cells, result.cells))
cwd = os.path.dirname(notebook_path)

html, resources = exporter.from_notebook_node(result, resources=resources)
@tornado.gen.coroutine
def kernel_start():
# Fetch kernel name from the notebook metadata
kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)

# Launch kernel and execute notebook
kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name, path=cwd))
return kernel_id

@tornado.gen.coroutine
def notebook_execute(nb, kernel_id):
km = self.kernel_manager.get_kernel(kernel_id)

if self.strip_sources:
exporter.exclude_input = True
exporter.exclude_output_prompt = True
exporter.exclude_input_prompt = True

# Filtering out empty cells.
def filter_empty_code_cells(cell):
return (
cell.cell_type != 'code' or # keep non-code cells
(cell.outputs and not exporter.exclude_output) # keep cell if output not excluded and not empty
or not exporter.exclude_input # keep cell if input not excluded
)
result = executenb(notebook, km=km, cwd=cwd)
result.cells = list(filter(filter_empty_code_cells, result.cells))
# we modify the notebook in place, since the nb variable cannot be reassigned it seems in jinja2
# e.g. if we do {% with nb = notebook_execute(nb, kernel_id) %}, the base template/blocks will not
# see the updated variable (it seems to be local to our block)
nb.cells = result.cells

# these functions allow the start of a kernel and execution of the notebook after (parts of) the template
# has been rendered and send to the client to allow progresssive rendering.
extra_context = {
'kernel_start': lambda: kernel_start().result(), # pass the result (not the future) to the template
'notebook_execute': notebook_execute
}

# Compose reply
self.set_header('Content-Type', 'text/html')
self.write(html)
# render notebook in snippets, and flush them out to the browser can render progresssively
for html_snippet, resources in exporter.generate_from_notebook_node(notebook, resources=resources, extra_context=extra_context):
self.write(html_snippet)
self.flush()
self.flush()
35 changes: 35 additions & 0 deletions voila/html.py
Expand Up @@ -3,6 +3,8 @@
import nbconvert.exporters.html
from jinja2 import contextfilter
from nbconvert.filters.markdown_mistune import IPythonRenderer, MarkdownWithMath
from nbconvert.exporters.templateexporter import TemplateExporter
from nbconvert.filters.highlight import Highlight2HTML


class VoilaMarkdownRenderer(IPythonRenderer):
Expand All @@ -27,3 +29,36 @@ def markdown2html(self, context, source):
contents_manager=self.contents_manager,
anchor_link_text=self.anchor_link_text)
return MarkdownWithMath(renderer=renderer).render(source)

def generate_from_notebook_node(self, nb, resources=None, extra_context={}, **kw):
# this replaces from_notebook_node, but calls template.generate instead of template.render
langinfo = nb.metadata.get('language_info', {})
lexer = langinfo.get('pygments_lexer', langinfo.get('name', None))
highlight_code = self.filters.get('highlight_code', Highlight2HTML(pygments_lexer=lexer, parent=self))
self.register_filter('highlight_code', highlight_code)

# NOTE: we don't call HTML or TemplateExporter' from_notebook_node
nb_copy, resources = super(TemplateExporter, self).from_notebook_node(nb, resources, **kw)
resources.setdefault('raw_mimetypes', self.raw_mimetypes)
resources['global_content_filter'] = {
'include_code': not self.exclude_code_cell,
'include_markdown': not self.exclude_markdown,
'include_raw': not self.exclude_raw,
'include_unknown': not self.exclude_unknown,
'include_input': not self.exclude_input,
'include_output': not self.exclude_output,
'include_input_prompt': not self.exclude_input_prompt,
'include_output_prompt': not self.exclude_output_prompt,
'no_prompt': self.exclude_input_prompt and self.exclude_output_prompt,
}

# Top level variables are passed to the template_exporter here.
for output in self.template.generate(nb=nb_copy, resources=resources, **extra_context):
yield output, resources

@property
def environment(self):
env = super(type(self), self).environment
if 'jinja2.ext.do' not in env.extensions:
env.add_extension('jinja2.ext.do')
return env

0 comments on commit 29f2f8a

Please sign in to comment.