exporter.py
441 lines
| 15.6 KiB
| text/x-python
|
PythonLexer
Jonathan Frederic
|
r10690 | """This module defines Exporter, a highly configurable converter | ||
that uses Jinja2 to export notebook files into different formats. | ||||
Matthias BUSSONNIER
|
r9578 | """ | ||
Matthias BUSSONNIER
|
r9819 | |||
Matthias BUSSONNIER
|
r9578 | #----------------------------------------------------------------------------- | ||
Matthias BUSSONNIER
|
r9665 | # Copyright (c) 2013, the IPython Development Team. | ||
Matthias BUSSONNIER
|
r9578 | # | ||
# Distributed under the terms of the Modified BSD License. | ||||
# | ||||
# The full license is in the file COPYING.txt, distributed with this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Jonathan Frederic
|
r10677 | |||
Jonathan Frederic
|
r10430 | from __future__ import print_function, absolute_import | ||
Matthias BUSSONNIER
|
r9665 | |||
# Stdlib imports | ||||
import io | ||||
Matthias BUSSONNIER
|
r9819 | import os | ||
Jonathan Frederic
|
r10631 | import inspect | ||
Jonathan Frederic
|
r11367 | import types | ||
Jonathan Frederic
|
r11383 | import copy | ||
import collections | ||||
import datetime | ||||
Matthias BUSSONNIER
|
r9665 | |||
Brian E. Granger
|
r11089 | # other libs/dependencies | ||
Matthias BUSSONNIER
|
r11279 | from jinja2 import Environment, FileSystemLoader, ChoiceLoader | ||
Brian E. Granger
|
r11089 | |||
Matthias BUSSONNIER
|
r9665 | # IPython imports | ||
from IPython.config.configurable import Configurable | ||||
Matthias BUSSONNIER
|
r10862 | from IPython.config import Config | ||
Matthias BUSSONNIER
|
r9665 | from IPython.nbformat import current as nbformat | ||
Jonathan Frederic
|
r11367 | from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict | ||
from IPython.utils.importstring import import_item | ||||
Jonathan Frederic
|
r10432 | from IPython.utils.text import indent | ||
Matthias BUSSONNIER
|
r9701 | |||
Jonathan Frederic
|
r11383 | from IPython.nbconvert import transformers as nbtransformers | ||
Brian E. Granger
|
r11089 | from IPython.nbconvert import filters | ||
Jonathan Frederic
|
r10430 | |||
Jonathan Frederic
|
r10431 | #----------------------------------------------------------------------------- | ||
# Globals and constants | ||||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r9665 | |||
Jonathan Frederic
|
r10431 | #Jinja2 extensions to load. | ||
JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols'] | ||||
Matthias BUSSONNIER
|
r10838 | default_filters = { | ||
'indent': indent, | ||||
MinRK
|
r11268 | 'markdown': filters.markdown2html, | ||
Brian E. Granger
|
r11089 | 'ansi2html': filters.ansi2html, | ||
'filter_data_type': filters.DataTypeFilter, | ||||
'get_lines': filters.get_lines, | ||||
'highlight': filters.highlight, | ||||
'highlight2html': filters.highlight, | ||||
'highlight2latex': filters.highlight2latex, | ||||
'markdown2latex': filters.markdown2latex, | ||||
'markdown2rst': filters.markdown2rst, | ||||
'pycomment': filters.python_comment, | ||||
'rm_ansi': filters.remove_ansi, | ||||
'rm_dollars': filters.strip_dollars, | ||||
'rm_fake': filters.rm_fake, | ||||
MinRK
|
r11302 | 'html_text' : filters.html_text, | ||
'add_anchor': filters.add_anchor, | ||||
Brian E. Granger
|
r11089 | 'ansi2latex': filters.ansi2latex, | ||
'rm_math_space': filters.rm_math_space, | ||||
'wrap': filters.wrap | ||||
Matthias BUSSONNIER
|
r10838 | } | ||
Jonathan Frederic
|
r10431 | #----------------------------------------------------------------------------- | ||
Jonathan Frederic
|
r10677 | # Class | ||
Matthias BUSSONNIER
|
r9665 | #----------------------------------------------------------------------------- | ||
Jonathan Frederic
|
r10677 | |||
Jonathan Frederic
|
r11398 | class ResourcesDict(collections.defaultdict): | ||
def __missing__(self, key): | ||||
return '' | ||||
Jonathan Frederic
|
r10430 | class Exporter(Configurable): | ||
Jonathan Frederic
|
r10690 | """ | ||
Exports notebooks into other file formats. Uses Jinja 2 templating engine | ||||
to output new formats. Inherit from this class if you are creating a new | ||||
template type along with new filters/transformers. If the filters/ | ||||
transformers provided by default suffice, there is no need to inherit from | ||||
this class. Instead, override the template_file and file_extension | ||||
traits via a config file. | ||||
Matthias BUSSONNIER
|
r10838 | {filters} | ||
""" | ||||
Matthias BUSSONNIER
|
r10874 | |||
# finish the docstring | ||||
Matthias BUSSONNIER
|
r10838 | __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys())) | ||
Jonathan Frederic
|
r10431 | template_file = Unicode( | ||
'', config=True, | ||||
help="Name of the template file to use") | ||||
Jonathan Frederic
|
r9768 | |||
Jonathan Frederic
|
r10587 | file_extension = Unicode( | ||
Jonathan Frederic
|
r10435 | 'txt', config=True, | ||
help="Extension of the file that should be written to disk" | ||||
) | ||||
Jonathan Frederic
|
r10690 | template_path = Unicode( | ||
MinRK
|
r11165 | os.path.join("..", "templates"), config=True, | ||
Jonathan Frederic
|
r10690 | help="Path where the template files are located.") | ||
template_skeleton_path = Unicode( | ||||
MinRK
|
r11165 | os.path.join("..", "templates", "skeleton"), config=True, | ||
Jonathan Frederic
|
r10690 | help="Path where the template skeleton files are located.") | ||
#Jinja block definitions | ||||
Jonathan Frederic
|
r10693 | jinja_comment_block_start = Unicode("", config=True) | ||
jinja_comment_block_end = Unicode("", config=True) | ||||
jinja_variable_block_start = Unicode("", config=True) | ||||
jinja_variable_block_end = Unicode("", config=True) | ||||
jinja_logic_block_start = Unicode("", config=True) | ||||
jinja_logic_block_end = Unicode("", config=True) | ||||
Jonathan Frederic
|
r10690 | |||
Jonathan Frederic
|
r10624 | #Extension that the template files use. | ||
Jonathan Frederic
|
r10690 | template_extension = Unicode(".tpl", config=True) | ||
Jonathan Frederic
|
r10624 | |||
Jonathan Frederic
|
r11367 | #Configurability, allows the user to easily add filters and transformers. | ||
transformers = List(config=True, | ||||
help="""List of transformers, by name or namespace, to enable.""") | ||||
Matthias BUSSONNIER
|
r9640 | |||
Jonathan Frederic
|
r11367 | filters = Dict(config=True, | ||
help="""Dictionary of filters, by name and namespace, to add to the Jinja | ||||
environment.""") | ||||
Jonathan Frederic
|
r11383 | |||
default_transformers = List([nbtransformers.coalesce_streams, | ||||
Jonathan Frederic
|
r11429 | nbtransformers.SVG2PDFTransformer, | ||
nbtransformers.ExtractFigureTransformer, | ||||
nbtransformers.CSSHTMLHeaderTransformer, | ||||
nbtransformers.RevealHelpTransformer, | ||||
nbtransformers.LatexTransformer, | ||||
nbtransformers.SphinxTransformer], | ||||
Jonathan Frederic
|
r11383 | config=True, | ||
help="""List of transformers available by default, by name, namespace, | ||||
instance, or type.""") | ||||
Jonathan Frederic
|
r11429 | |||
Jonathan Frederic
|
r11367 | def __init__(self, config=None, extra_loaders=None, **kw): | ||
Jonathan Frederic
|
r10690 | """ | ||
Public constructor | ||||
Jonathan Frederic
|
r10485 | |||
Jonathan Frederic
|
r10690 | Parameters | ||
---------- | ||||
config : config | ||||
User configuration instance. | ||||
Matthias BUSSONNIER
|
r11279 | extra_loaders : list[of Jinja Loaders] | ||
ordered list of Jinja loder to find templates. Will be tried in order | ||||
before the default FileSysteme ones. | ||||
Jonathan Frederic
|
r10690 | """ | ||
Jonathan Frederic
|
r10435 | #Call the base class constructor | ||
Matthias BUSSONNIER
|
r10963 | c = self.default_config | ||
if config: | ||||
c.merge(config) | ||||
super(Exporter, self).__init__(config=c, **kw) | ||||
Matthias BUSSONNIER
|
r9665 | |||
Jonathan Frederic
|
r11383 | #Init | ||
Matthias BUSSONNIER
|
r11279 | self._init_environment(extra_loaders=extra_loaders) | ||
Jonathan Frederic
|
r11383 | self._init_transformers() | ||
self._init_filters() | ||||
Matthias BUSSONNIER
|
r9624 | |||
Matthias BUSSONNIER
|
r10862 | |||
@property | ||||
def default_config(self): | ||||
Matthias BUSSONNIER
|
r10963 | return Config() | ||
Jonathan Frederic
|
r10588 | |||
Jonathan Frederic
|
r11376 | def from_notebook_node(self, nb, resources=None, **kw): | ||
Jonathan Frederic
|
r10690 | """ | ||
Convert a notebook from a notebook node instance. | ||||
Parameters | ||||
---------- | ||||
nb : Notebook node | ||||
Jonathan Frederic
|
r11367 | resources : dict (**kw) | ||
of additional resources that can be accessed read/write by | ||||
transformers and filters. | ||||
Jonathan Frederic
|
r10690 | """ | ||
Jonathan Frederic
|
r11383 | nb_copy = copy.deepcopy(nb) | ||
resources = self._init_resources(resources) | ||||
Jonathan Frederic
|
r11379 | |||
Jonathan Frederic
|
r11367 | #Preprocess | ||
Jonathan Frederic
|
r11379 | nb_copy, resources = self._transform(nb_copy, resources) | ||
Matthias BUSSONNIER
|
r9578 | |||
Jonathan Frederic
|
r11367 | #Convert | ||
self.template = self.environment.get_template(self.template_file + self.template_extension) | ||||
Jonathan Frederic
|
r11375 | output = self.template.render(nb=nb_copy, resources=resources) | ||
Jonathan Frederic
|
r11367 | return output, resources | ||
Matthias BUSSONNIER
|
r9578 | |||
Jonathan Frederic
|
r11367 | |||
Jonathan Frederic
|
r11379 | def from_filename(self, filename, resources=None, **kw): | ||
Jonathan Frederic
|
r10690 | """ | ||
Convert a notebook from a notebook file. | ||||
Parameters | ||||
---------- | ||||
filename : str | ||||
Full filename of the notebook file to open and convert. | ||||
""" | ||||
Jonathan Frederic
|
r11383 | |||
#Pull the metadata from the filesystem. | ||||
Jonathan Frederic
|
r11425 | if resources is None: | ||
resources = ResourcesDict() | ||||
if not 'metadata' in resources or resources['metadata'] == '': | ||||
Jonathan Frederic
|
r11383 | resources['metadata'] = ResourcesDict() | ||
basename = os.path.basename(filename) | ||||
notebook_name = basename[:basename.rfind('.')] | ||||
resources['metadata']['name'] = notebook_name | ||||
modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename)) | ||||
resources['metadata']['modified_date'] = modified_date.strftime("%B %-d, %Y") | ||||
Jonathan Frederic
|
r10690 | |||
Jonathan Frederic
|
r10431 | with io.open(filename) as f: | ||
Jonathan Frederic
|
r11367 | return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw) | ||
Jonathan Frederic
|
r10431 | |||
Jonathan Frederic
|
r10432 | |||
Jonathan Frederic
|
r11376 | def from_file(self, file_stream, resources=None, **kw): | ||
Jonathan Frederic
|
r10690 | """ | ||
Convert a notebook from a notebook file. | ||||
Parameters | ||||
---------- | ||||
file_stream : file-like object | ||||
Notebook file-like object to convert. | ||||
""" | ||||
Jonathan Frederic
|
r11367 | return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw) | ||
Jonathan Frederic
|
r10578 | |||
Jonathan Frederic
|
r11428 | def register_transformer(self, transformer, enabled=False): | ||
Jonathan Frederic
|
r10690 | """ | ||
Register a transformer. | ||||
Transformers are classes that act upon the notebook before it is | ||||
passed into the Jinja templating engine. Transformers are also | ||||
capable of passing additional information to the Jinja | ||||
templating engine. | ||||
Parameters | ||||
---------- | ||||
transformer : transformer | ||||
""" | ||||
Jonathan Frederic
|
r11428 | if transformer is None: | ||
raise TypeError('transformer') | ||||
isclass = inspect.isclass(transformer) | ||||
constructed = not isclass | ||||
Jonathan Frederic
|
r11367 | |||
#Handle transformer's registration based on it's type | ||||
Jonathan Frederic
|
r11428 | if constructed and isinstance(transformer, types.StringTypes): | ||
#Transformer is a string, import the namespace and recursively call | ||||
#this register_transformer method | ||||
transformer_cls = import_item(transformer) | ||||
return self.register_transformer(transformer_cls, enabled) | ||||
Jonathan Frederic
|
r10690 | |||
Jonathan Frederic
|
r11428 | if constructed and hasattr(transformer, '__call__'): | ||
Jonathan Frederic
|
r11367 | #Transformer is a function, no need to construct it. | ||
Jonathan Frederic
|
r11428 | #Register and return the transformer. | ||
if enabled: | ||||
transformer.enabled = True | ||||
Jonathan Frederic
|
r11367 | self._transformers.append(transformer) | ||
Jonathan Frederic
|
r10631 | return transformer | ||
Jonathan Frederic
|
r11367 | |||
Jonathan Frederic
|
r11428 | elif isclass and isinstance(transformer, MetaHasTraits): | ||
Jonathan Frederic
|
r11367 | #Transformer is configurable. Make sure to pass in new default for | ||
#the enabled flag if one was specified. | ||||
Jonathan Frederic
|
r11428 | self.register_transformer(transformer(parent=self), enabled) | ||
Jonathan Frederic
|
r11367 | |||
Jonathan Frederic
|
r11428 | elif isclass: | ||
Jonathan Frederic
|
r11367 | #Transformer is not configurable, construct it | ||
Jonathan Frederic
|
r11428 | self.register_transformer(transformer(), enabled) | ||
Jonathan Frederic
|
r11367 | |||
Jonathan Frederic
|
r10587 | else: | ||
Jonathan Frederic
|
r11428 | #Transformer is an instance of something without a __call__ | ||
#attribute. | ||||
raise TypeError('transformer') | ||||
Jonathan Frederic
|
r10587 | |||
Jonathan Frederic
|
r10578 | def register_filter(self, name, filter): | ||
Jonathan Frederic
|
r10690 | """ | ||
Register a filter. | ||||
A filter is a function that accepts and acts on one string. | ||||
The filters are accesible within the Jinja templating engine. | ||||
Parameters | ||||
---------- | ||||
name : str | ||||
name to give the filter in the Jinja engine | ||||
filter : filter | ||||
""" | ||||
Jonathan Frederic
|
r11428 | if filter is None: | ||
raise TypeError('filter') | ||||
isclass = inspect.isclass(filter) | ||||
constructed = not isclass | ||||
#Handle filter's registration based on it's type | ||||
if constructed and isinstance(filter, types.StringTypes): | ||||
#filter is a string, import the namespace and recursively call | ||||
#this register_filter method | ||||
filter_cls = import_item(filter) | ||||
return self.register_filter(name, filter_cls) | ||||
if constructed and hasattr(filter, '__call__'): | ||||
#filter is a function, no need to construct it. | ||||
Jonathan Frederic
|
r10631 | self.environment.filters[name] = filter | ||
Jonathan Frederic
|
r11428 | return filter | ||
elif isclass and isinstance(filter, MetaHasTraits): | ||||
#filter is configurable. Make sure to pass in new default for | ||||
#the enabled flag if one was specified. | ||||
self.register_filter(name, filter(parent=self)) | ||||
elif isclass: | ||||
#filter is not configurable, construct it | ||||
self.register_filter(name, filter()) | ||||
Jonathan Frederic
|
r10578 | else: | ||
Jonathan Frederic
|
r11428 | #filter is an instance of something without a __call__ | ||
#attribute. | ||||
raise TypeError('filter') | ||||
Jonathan Frederic
|
r10578 | |||
Jonathan Frederic
|
r10588 | |||
Matthias BUSSONNIER
|
r11279 | def _init_environment(self, extra_loaders=None): | ||
Jonathan Frederic
|
r10690 | """ | ||
Create the Jinja templating environment. | ||||
""" | ||||
MinRK
|
r11197 | here = os.path.dirname(os.path.realpath(__file__)) | ||
Matthias BUSSONNIER
|
r11279 | loaders = [] | ||
if extra_loaders: | ||||
loaders.extend(extra_loaders) | ||||
loaders.append(FileSystemLoader([ | ||||
MinRK
|
r11165 | os.path.join(here, self.template_path), | ||
os.path.join(here, self.template_skeleton_path), | ||||
Matthias BUSSONNIER
|
r11279 | ])) | ||
self.environment = Environment( | ||||
loader= ChoiceLoader(loaders), | ||||
Jonathan Frederic
|
r10485 | extensions=JINJA_EXTENSIONS | ||
) | ||||
Jonathan Frederic
|
r10690 | |||
#Set special Jinja2 syntax that will not conflict with latex. | ||||
Jonathan Frederic
|
r10693 | if self.jinja_logic_block_start: | ||
Jonathan Frederic
|
r10690 | self.environment.block_start_string = self.jinja_logic_block_start | ||
Jonathan Frederic
|
r10693 | if self.jinja_logic_block_end: | ||
Jonathan Frederic
|
r10690 | self.environment.block_end_string = self.jinja_logic_block_end | ||
Jonathan Frederic
|
r10693 | if self.jinja_variable_block_start: | ||
Jonathan Frederic
|
r10690 | self.environment.variable_start_string = self.jinja_variable_block_start | ||
Jonathan Frederic
|
r10693 | if self.jinja_variable_block_end: | ||
Jonathan Frederic
|
r10690 | self.environment.variable_end_string = self.jinja_variable_block_end | ||
Jonathan Frederic
|
r10693 | if self.jinja_comment_block_start: | ||
Jonathan Frederic
|
r10690 | self.environment.comment_start_string = self.jinja_comment_block_start | ||
Jonathan Frederic
|
r10693 | if self.jinja_comment_block_end: | ||
Jonathan Frederic
|
r10690 | self.environment.comment_end_string = self.jinja_comment_block_end | ||
Jonathan Frederic
|
r10485 | |||
Jonathan Frederic
|
r11383 | |||
def _init_transformers(self): | ||||
""" | ||||
Register all of the transformers needed for this exporter, disabled | ||||
unless specified explicitly. | ||||
""" | ||||
self._transformers = [] | ||||
#Load default transformers (not necessarly enabled by default). | ||||
if self.default_transformers: | ||||
for transformer in self.default_transformers: | ||||
self.register_transformer(transformer) | ||||
#Load user transformers. Enable by default. | ||||
if self.transformers: | ||||
for transformer in self.transformers: | ||||
self.register_transformer(transformer, enabled=True) | ||||
def _init_filters(self): | ||||
""" | ||||
Register all of the filters required for the exporter. | ||||
""" | ||||
#Add default filters to the Jinja2 environment | ||||
for key, value in default_filters.iteritems(): | ||||
self.register_filter(key, value) | ||||
#Load user filters. Overwrite existing filters if need be. | ||||
if self.filters: | ||||
for key, user_filter in self.filters.iteritems(): | ||||
self.register_filter(key, user_filter) | ||||
def _init_resources(self, resources): | ||||
Jonathan Frederic
|
r10485 | |||
Jonathan Frederic
|
r11383 | #Make sure the resources dict is of ResourcesDict type. | ||
if resources is None: | ||||
resources = ResourcesDict() | ||||
if not isinstance(resources, ResourcesDict): | ||||
new_resources = ResourcesDict() | ||||
new_resources.update(resources) | ||||
resources = new_resources | ||||
#Make sure the metadata extension exists in resources | ||||
if 'metadata' in resources: | ||||
if not isinstance(resources['metadata'], ResourcesDict): | ||||
resources['metadata'] = ResourcesDict(resources['metadata']) | ||||
else: | ||||
resources['metadata'] = ResourcesDict() | ||||
Jonathan Frederic
|
r11402 | if not resources['metadata']['name']: | ||
Jonathan Frederic
|
r11403 | resources['metadata']['name'] = 'Notebook' | ||
Jonathan Frederic
|
r11383 | |||
#Set the output extension | ||||
resources['output_extension'] = self.file_extension | ||||
return resources | ||||
Jonathan Frederic
|
r10485 | |||
Jonathan Frederic
|
r11379 | def _transform(self, nb, resources): | ||
Jonathan Frederic
|
r10690 | """ | ||
Preprocess the notebook before passing it into the Jinja engine. | ||||
To preprocess the notebook is to apply all of the | ||||
Parameters | ||||
---------- | ||||
nb : notebook node | ||||
notebook that is being exported. | ||||
Matthias BUSSONNIER
|
r10837 | resources : a dict of additional resources that | ||
can be accessed read/write by transformers | ||||
and filters. | ||||
Jonathan Frederic
|
r10690 | """ | ||
Jonathan Frederic
|
r11399 | # Do a copy.deepcopy first, | ||
Matthias BUSSONNIER
|
r10867 | # we are never safe enough with what the transformers could do. | ||
Jonathan Frederic
|
r11399 | nbc = copy.deepcopy(nb) | ||
resc = copy.deepcopy(resources) | ||||
Jonathan Frederic
|
r11367 | |||
Jonathan Frederic
|
r10432 | #Run each transformer on the notebook. Carry the output along | ||
#to each transformer | ||||
Jonathan Frederic
|
r11367 | for transformer in self._transformers: | ||
nbc, resc = transformer(nbc, resc) | ||||
return nbc, resc | ||||