diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 019850cb3..d1d8e9fdf 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -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 @@ -57,7 +58,6 @@ 'json_dumps': json.dumps, } - class ExtensionTolerantLoader(BaseLoader): """A template loader which optionally adds a given extension when searching. @@ -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 = "" + @observe('template_file') def _template_file_changed(self, change): new = change['new'] @@ -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) @@ -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) @@ -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.", @@ -229,7 +240,7 @@ def _raw_mimetypes_default(self): def __init__(self, config=None, **kw): """ Public constructor - + Parameters ---------- config : config @@ -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!") @@ -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), diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 6496b956e..0a57b6ebe 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -7,6 +7,7 @@ import os +from traitlets import default from traitlets.config import Config from jinja2 import DictLoader, TemplateNotFound from nbformat import v4 @@ -14,12 +15,19 @@ 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""" @@ -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":{ @@ -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, @@ -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":{ @@ -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= { @@ -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":{