Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raw template as settable attribute #675

Merged
merged 21 commits into from
Oct 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5ce4ca7
handle raw_template via attribute
mpacer Sep 13, 2017
cc03ee7
uses raw_template traitlet, creates a register helper and observe
mpacer Sep 14, 2017
4db83b4
check if raw_template is set inside _load_template
mpacer Sep 14, 2017
8b027d2
Add test for 4 ways of setting a raw_template in a class
mpacer Sep 14, 2017
dbd0488
Remove affects_template tag, store default_template, restore if no ra…
mpacer Sep 14, 2017
810d077
Add test to verify that an empty raw_template returns back to dynamic…
mpacer Sep 14, 2017
c3cfde8
use attribute for _raw_template_key as a precursor to not appending D…
mpacer Sep 14, 2017
449ae22
Change dynamic test to only work if set as a traitlet per @takluyver'…
mpacer Sep 14, 2017
ea52c91
use FunctionLoader not DictLoader so we can mutate values inside the …
mpacer Sep 14, 2017
b335d9c
split up tests and explicitly test for traitlet related order effects
mpacer Sep 14, 2017
728c15b
clean up tests, remove old in memory test that is now super redundant
mpacer Sep 14, 2017
6157733
add docstrings to all new tests
mpacer Sep 14, 2017
b79f0d8
simplify logic using hold_trait_notifications() ftw (avoids race cond…
mpacer Sep 14, 2017
d1bbd81
make raw_template_key configurable as backdoor in the case of filenam…
mpacer Sep 14, 2017
738673a
add test for reassignment
mpacer Sep 16, 2017
2975c1b
Make _last_template_file a simple attribute, invalidate cache when se…
mpacer Sep 16, 2017
34074a5
use dictloader and add back affects_environment tag to raw_template
mpacer Sep 16, 2017
c900829
don't make raw_template_key configurable or public
mpacer Sep 16, 2017
d23c7e0
test deassignment on a non-custom exporter
mpacer Sep 16, 2017
7a92360
Handle reassigning and then deassigning new raw templates and getting…
mpacer Sep 17, 2017
8b95c29
remove unnecessary _load_raw_template method
mpacer Sep 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions nbconvert/exporters/templateexporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from traitlets.utils.importstring import import_item
from ipython_genutils import py3compat
from jinja2 import (
TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader
TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader,
DictLoader
)

from nbconvert import filters
Expand Down Expand Up @@ -57,7 +58,6 @@
'json_dumps': json.dumps,
}


class ExtensionTolerantLoader(BaseLoader):
"""A template loader which optionally adds a given extension when searching.

Expand Down Expand Up @@ -140,6 +140,11 @@ def default_config(self):
help="Name of the template file to use"
).tag(config=True, affects_template=True)

raw_template = Unicode('', help="raw template string").tag(affects_environment=True)

_last_template_file = ""
_raw_template_key = "<memory>"

@observe('template_file')
def _template_file_changed(self, change):
new = change['new']
Expand All @@ -159,6 +164,12 @@ def _template_file_changed(self, change):
def _template_file_default(self):
return self.default_template

@observe('raw_template')
def _raw_template_changed(self, change):
if not change['new']:
self.template_file = self.default_template or self._last_template_file
self._invalidate_template_cache()

default_template = Unicode(u'').tag(affects_template=True)

template_path = List(['.']).tag(config=True, affects_environment=True)
Expand All @@ -172,10 +183,10 @@ def _template_file_default(self):
os.path.join("..", "templates", "skeleton"),
help="Path where the template skeleton files are located.",
).tag(affects_environment=True)

#Extension that the template files use.
template_extension = Unicode(".tpl").tag(config=True, affects_environment=True)

exclude_input = Bool(False,
help = "This allows you to exclude code cell inputs from all templates if set to True."
).tag(config=True)
Expand Down Expand Up @@ -207,7 +218,7 @@ def _template_file_default(self):
exclude_unknown = Bool(False,
help = "This allows you to exclude unknown cells from all templates if set to True."
).tag(config=True)

extra_loaders = List(
help="Jinja loaders to find templates. Will be tried in order "
"before the default FileSystem ones.",
Expand All @@ -229,7 +240,7 @@ def _raw_mimetypes_default(self):
def __init__(self, config=None, **kw):
"""
Public constructor

Parameters
----------
config : config
Expand All @@ -250,10 +261,17 @@ def __init__(self, config=None, **kw):

def _load_template(self):
"""Load the Jinja template object from the template file

This is triggered by various trait changes that would change the template.
"""

# this gives precedence to a raw_template if present
with self.hold_trait_notifications():
if self.template_file != self._raw_template_key:
self._last_template_file = self.template_file
if self.raw_template:
self.template_file = self._raw_template_key

if not self.template_file:
raise ValueError("No template_file specified!")

Expand Down Expand Up @@ -377,7 +395,8 @@ def _create_environment(self):
os.path.join(here, self.template_skeleton_path)]

loaders = self.extra_loaders + [
ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension)
ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension),
DictLoader({self._raw_template_key: self.raw_template})
]
environment = Environment(
loader=ChoiceLoader(loaders),
Expand Down
201 changes: 180 additions & 21 deletions nbconvert/exporters/tests/test_templateexporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@

import os

from traitlets import default
from traitlets.config import Config
from jinja2 import DictLoader, TemplateNotFound
from nbformat import v4

from .base import ExportersTestsBase
from .cheese import CheesePreprocessor
from ..templateexporter import TemplateExporter
from ..rst import RSTExporter
from ..html import HTMLExporter
from ..markdown import MarkdownExporter
from testpath import tempdir

import pytest

raw_template = """{%- extends 'rst.tpl' -%}
{%- block in_prompt -%}
blah
{%- endblock in_prompt -%}
"""

class TestExporter(ExportersTestsBase):
"""Contains test functions for exporter.py"""

Expand Down Expand Up @@ -119,33 +127,184 @@ def test_relative_template_file(self):
exporter = self._make_exporter(config=config)
assert os.path.abspath(exporter.template.filename) == template
assert os.path.dirname(template) in [os.path.abspath(d) for d in exporter.template_path]

def test_in_memory_template(self):
# Loads in an in memory template using jinja2.DictLoader
# creates a class that uses this template with the template_file argument
# converts an empty notebook using this mechanism
my_loader = DictLoader({'my_template': "{%- extends 'rst.tpl' -%}"})

class MyExporter(TemplateExporter):
template_file = 'my_template'

exporter = MyExporter(extra_loaders=[my_loader])


def test_raw_template_attr(self):
"""
Verify that you can assign a in memory template string by overwriting
`raw_template` as simple(non-traitlet) attribute
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))

class AttrExporter(TemplateExporter):
raw_template = raw_template

exporter_attr = AttrExporter()
output_attr, _ = exporter_attr.from_notebook_node(nb)
assert "blah" in output_attr

def test_raw_template_init(self):
"""
Test that template_file and raw_template traitlets play nicely together.
- source assigns template_file default first, then raw_template
- checks that the raw_template overrules template_file if set
- checks that once raw_template is set to '', template_file returns
"""
nb = v4.new_notebook()
out, resources = exporter.from_notebook_node(nb)
nb.cells.append(v4.new_code_cell("some_text"))

class AttrExporter(RSTExporter):

def __init__(self, *args, **kwargs):
self.raw_template = raw_template

exporter_init = AttrExporter()
output_init, _ = exporter_init.from_notebook_node(nb)
assert "blah" in output_init
exporter_init.raw_template = ''
assert exporter_init.template_file == "rst.tpl"
output_init, _ = exporter_init.from_notebook_node(nb)
assert "blah" not in output_init

def test_raw_template_dynamic_attr(self):
"""
Test that template_file and raw_template traitlets play nicely together.
- source assigns template_file default first, then raw_template
- checks that the raw_template overrules template_file if set
- checks that once raw_template is set to '', template_file returns
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))

class AttrDynamicExporter(TemplateExporter):
@default('template_file')
def _template_file_default(self):
return "rst.tpl"

@default('raw_template')
def _raw_template_default(self):
return raw_template

exporter_attr_dynamic = AttrDynamicExporter()
output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb)
assert "blah" in output_attr_dynamic
exporter_attr_dynamic.raw_template = ''
assert exporter_attr_dynamic.template_file == "rst.tpl"
output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb)
assert "blah" not in output_attr_dynamic

def test_raw_template_dynamic_attr_reversed(self):
"""
Test that template_file and raw_template traitlets play nicely together.
- source assigns raw_template default first, then template_file
- checks that the raw_template overrules template_file if set
- checks that once raw_template is set to '', template_file returns
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))

class AttrDynamicExporter(TemplateExporter):
@default('raw_template')
def _raw_template_default(self):
return raw_template

@default('template_file')
def _template_file_default(self):
return "rst.tpl"

exporter_attr_dynamic = AttrDynamicExporter()
output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb)
assert "blah" in output_attr_dynamic
exporter_attr_dynamic.raw_template = ''
assert exporter_attr_dynamic.template_file == "rst.tpl"
output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb)
assert "blah" not in output_attr_dynamic


def test_raw_template_constructor(self):
"""
Test `raw_template` as a keyword argument in the exporter constructor.
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))

output_constructor, _ = TemplateExporter(
raw_template=raw_template).from_notebook_node(nb)
assert "blah" in output_constructor

def test_raw_template_assignment(self):
"""
Test `raw_template` assigned after the fact on non-custom Exporter.
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))
exporter_assign = TemplateExporter()
exporter_assign.raw_template = raw_template
output_assign, _ = exporter_assign.from_notebook_node(nb)
assert "blah" in output_assign

def test_raw_template_reassignment(self):
"""
Test `raw_template` reassigned after the fact on non-custom Exporter.
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))
exporter_reassign = TemplateExporter()
exporter_reassign.raw_template = raw_template
output_reassign, _ = exporter_reassign.from_notebook_node(nb)
assert "blah" in output_reassign
exporter_reassign.raw_template = raw_template.replace("blah", "baz")
output_reassign, _ = exporter_reassign.from_notebook_node(nb)
assert "baz" in output_reassign

def test_raw_template_deassignment(self):
"""
Test `raw_template` does not overwrite template_file if deassigned after
being assigned to a non-custom Exporter.
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))
exporter_deassign = RSTExporter()
exporter_deassign.raw_template = raw_template
output_deassign, _ = exporter_deassign.from_notebook_node(nb)
assert "blah" in output_deassign
exporter_deassign.raw_template = ''
assert exporter_deassign.template_file == 'rst.tpl'
output_deassign, _ = exporter_deassign.from_notebook_node(nb)
assert "blah" not in output_deassign

def test_raw_template_dereassignment(self):
"""
Test `raw_template` does not overwrite template_file if deassigned after
being assigned to a non-custom Exporter.
"""
nb = v4.new_notebook()
nb.cells.append(v4.new_code_cell("some_text"))
exporter_dereassign = RSTExporter()
exporter_dereassign.raw_template = raw_template
output_dereassign, _ = exporter_dereassign.from_notebook_node(nb)
assert "blah" in output_dereassign
exporter_dereassign.raw_template = raw_template.replace("blah", "baz")
output_dereassign, _ = exporter_dereassign.from_notebook_node(nb)
assert "baz" in output_dereassign
exporter_dereassign.raw_template = ''
assert exporter_dereassign.template_file == 'rst.tpl'
output_dereassign, _ = exporter_dereassign.from_notebook_node(nb)
assert "blah" not in output_dereassign

def test_fail_to_find_template_file(self):
# Create exporter with invalid template file, check that it doesn't
# exist in the environment, try to convert empty notebook. Failure is
# expected due to nonexistant template file.

template = 'does_not_exist.tpl'
exporter = TemplateExporter(template_file=template)
assert template not in exporter.environment.list_templates(extensions=['tpl'])
nb = v4.new_notebook()
with pytest.raises(TemplateNotFound):
out, resources = exporter.from_notebook_node(nb)

def test_exclude_code_cell(self):
no_io = {
"TemplateExporter":{
Expand All @@ -161,10 +320,10 @@ def test_exclude_code_cell(self):
exporter_no_io = TemplateExporter(config=c_no_io)
exporter_no_io.template_file = 'markdown'
nb_no_io, resources_no_io = exporter_no_io.from_filename(self._get_notebook())

assert not resources_no_io['global_content_filter']['include_input']
assert not resources_no_io['global_content_filter']['include_output']

no_code = {
"TemplateExporter":{
"exclude_output": False,
Expand All @@ -183,7 +342,7 @@ def test_exclude_code_cell(self):
assert not resources_no_code['global_content_filter']['include_code']
assert nb_no_io == nb_no_code


def test_exclude_input_prompt(self):
no_input_prompt = {
"TemplateExporter":{
Expand All @@ -198,10 +357,10 @@ def test_exclude_input_prompt(self):
c_no_input_prompt = Config(no_input_prompt)
exporter_no_input_prompt = MarkdownExporter(config=c_no_input_prompt)
nb_no_input_prompt, resources_no_input_prompt = exporter_no_input_prompt.from_filename(self._get_notebook())

assert not resources_no_input_prompt['global_content_filter']['include_input_prompt']
assert "# In[" not in nb_no_input_prompt

def test_exclude_markdown(self):

no_md= {
Expand All @@ -219,10 +378,10 @@ def test_exclude_markdown(self):
exporter_no_md = TemplateExporter(config=c_no_md)
exporter_no_md.template_file = 'python'
nb_no_md, resources_no_md = exporter_no_md.from_filename(self._get_notebook())

assert not resources_no_md['global_content_filter']['include_markdown']
assert "First import NumPy and Matplotlib" not in nb_no_md

def test_exclude_output_prompt(self):
no_output_prompt = {
"TemplateExporter":{
Expand Down