##// END OF EJS Templates
Only set name if it doesn't exist!
Jonathan Frederic -
Show More
@@ -1,405 +1,405
1 1 """This module defines Exporter, a highly configurable converter
2 2 that uses Jinja2 to export notebook files into different formats.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 from __future__ import print_function, absolute_import
18 18
19 19 # Stdlib imports
20 20 import io
21 21 import os
22 22 import inspect
23 23 import types
24 24 import copy
25 25 import collections
26 26 import datetime
27 27
28 28 # other libs/dependencies
29 29 from jinja2 import Environment, FileSystemLoader, ChoiceLoader
30 30
31 31 # IPython imports
32 32 from IPython.config.configurable import Configurable
33 33 from IPython.config import Config
34 34 from IPython.nbformat import current as nbformat
35 35 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict
36 36 from IPython.utils.importstring import import_item
37 37 from IPython.utils.text import indent
38 38
39 39 from IPython.nbconvert import transformers as nbtransformers
40 40 from IPython.nbconvert import filters
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # Globals and constants
44 44 #-----------------------------------------------------------------------------
45 45
46 46 #Jinja2 extensions to load.
47 47 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
48 48
49 49 default_filters = {
50 50 'indent': indent,
51 51 'markdown': filters.markdown2html,
52 52 'ansi2html': filters.ansi2html,
53 53 'filter_data_type': filters.DataTypeFilter,
54 54 'get_lines': filters.get_lines,
55 55 'highlight': filters.highlight,
56 56 'highlight2html': filters.highlight,
57 57 'highlight2latex': filters.highlight2latex,
58 58 'markdown2latex': filters.markdown2latex,
59 59 'markdown2rst': filters.markdown2rst,
60 60 'pycomment': filters.python_comment,
61 61 'rm_ansi': filters.remove_ansi,
62 62 'rm_dollars': filters.strip_dollars,
63 63 'rm_fake': filters.rm_fake,
64 64 'ansi2latex': filters.ansi2latex,
65 65 'rm_math_space': filters.rm_math_space,
66 66 'wrap': filters.wrap
67 67 }
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # Class
71 71 #-----------------------------------------------------------------------------
72 72
73 73 class ResourcesDict(collections.defaultdict):
74 74 def __missing__(self, key):
75 75 return ''
76 76
77 77
78 78 class Exporter(Configurable):
79 79 """
80 80 Exports notebooks into other file formats. Uses Jinja 2 templating engine
81 81 to output new formats. Inherit from this class if you are creating a new
82 82 template type along with new filters/transformers. If the filters/
83 83 transformers provided by default suffice, there is no need to inherit from
84 84 this class. Instead, override the template_file and file_extension
85 85 traits via a config file.
86 86
87 87 {filters}
88 88 """
89 89
90 90 # finish the docstring
91 91 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
92 92
93 93
94 94 template_file = Unicode(
95 95 '', config=True,
96 96 help="Name of the template file to use")
97 97
98 98 file_extension = Unicode(
99 99 'txt', config=True,
100 100 help="Extension of the file that should be written to disk"
101 101 )
102 102
103 103 template_path = Unicode(
104 104 os.path.join("..", "templates"), config=True,
105 105 help="Path where the template files are located.")
106 106
107 107 template_skeleton_path = Unicode(
108 108 os.path.join("..", "templates", "skeleton"), config=True,
109 109 help="Path where the template skeleton files are located.")
110 110
111 111 #Jinja block definitions
112 112 jinja_comment_block_start = Unicode("", config=True)
113 113 jinja_comment_block_end = Unicode("", config=True)
114 114 jinja_variable_block_start = Unicode("", config=True)
115 115 jinja_variable_block_end = Unicode("", config=True)
116 116 jinja_logic_block_start = Unicode("", config=True)
117 117 jinja_logic_block_end = Unicode("", config=True)
118 118
119 119 #Extension that the template files use.
120 120 template_extension = Unicode(".tpl", config=True)
121 121
122 122 #Configurability, allows the user to easily add filters and transformers.
123 123 transformers = List(config=True,
124 124 help="""List of transformers, by name or namespace, to enable.""")
125 125
126 126 filters = Dict(config=True,
127 127 help="""Dictionary of filters, by name and namespace, to add to the Jinja
128 128 environment.""")
129 129
130 130 default_transformers = List([nbtransformers.coalesce_streams,
131 131 nbtransformers.ExtractFigureTransformer],
132 132 config=True,
133 133 help="""List of transformers available by default, by name, namespace,
134 134 instance, or type.""")
135 135
136 136 def __init__(self, config=None, extra_loaders=None, **kw):
137 137 """
138 138 Public constructor
139 139
140 140 Parameters
141 141 ----------
142 142 config : config
143 143 User configuration instance.
144 144 extra_loaders : list[of Jinja Loaders]
145 145 ordered list of Jinja loder to find templates. Will be tried in order
146 146 before the default FileSysteme ones.
147 147 """
148 148
149 149 #Call the base class constructor
150 150 c = self.default_config
151 151 if config:
152 152 c.merge(config)
153 153
154 154 super(Exporter, self).__init__(config=c, **kw)
155 155
156 156 #Init
157 157 self._init_environment(extra_loaders=extra_loaders)
158 158 self._init_transformers()
159 159 self._init_filters()
160 160
161 161
162 162 @property
163 163 def default_config(self):
164 164 return Config()
165 165
166 166
167 167 def from_notebook_node(self, nb, resources=None, **kw):
168 168 """
169 169 Convert a notebook from a notebook node instance.
170 170
171 171 Parameters
172 172 ----------
173 173 nb : Notebook node
174 174 resources : dict (**kw)
175 175 of additional resources that can be accessed read/write by
176 176 transformers and filters.
177 177 """
178 178 nb_copy = copy.deepcopy(nb)
179 179 resources = self._init_resources(resources)
180 180
181 181 #Preprocess
182 182 nb_copy, resources = self._transform(nb_copy, resources)
183 183
184 184 #Convert
185 185 self.template = self.environment.get_template(self.template_file + self.template_extension)
186 186 output = self.template.render(nb=nb_copy, resources=resources)
187 187 return output, resources
188 188
189 189
190 190 def from_filename(self, filename, resources=None, **kw):
191 191 """
192 192 Convert a notebook from a notebook file.
193 193
194 194 Parameters
195 195 ----------
196 196 filename : str
197 197 Full filename of the notebook file to open and convert.
198 198 """
199 199
200 200 #Pull the metadata from the filesystem.
201 201 if not 'metadata' in resources:
202 202 resources['metadata'] = ResourcesDict()
203 203 basename = os.path.basename(filename)
204 204 notebook_name = basename[:basename.rfind('.')]
205 205 resources['metadata']['name'] = notebook_name
206 206
207 207 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
208 208 resources['metadata']['modified_date'] = modified_date.strftime("%B %-d, %Y")
209 209
210 210 with io.open(filename) as f:
211 211 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
212 212
213 213
214 214 def from_file(self, file_stream, resources=None, **kw):
215 215 """
216 216 Convert a notebook from a notebook file.
217 217
218 218 Parameters
219 219 ----------
220 220 file_stream : file-like object
221 221 Notebook file-like object to convert.
222 222 """
223 223 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
224 224
225 225
226 226 def register_transformer(self, transformer, enabled=None):
227 227 """
228 228 Register a transformer.
229 229 Transformers are classes that act upon the notebook before it is
230 230 passed into the Jinja templating engine. Transformers are also
231 231 capable of passing additional information to the Jinja
232 232 templating engine.
233 233
234 234 Parameters
235 235 ----------
236 236 transformer : transformer
237 237 """
238 238
239 239 #Handle transformer's registration based on it's type
240 240 if inspect.isfunction(transformer):
241 241 #Transformer is a function, no need to construct it.
242 242 self._transformers.append(transformer)
243 243 return transformer
244 244
245 245 elif isinstance(transformer, types.StringTypes):
246 246 #Transformer is a string, import the namespace and recursively call
247 247 #this register_transformer method
248 248 transformer_cls = import_item(DottedObjectName(transformer))
249 249 return self.register_transformer(transformer_cls, enabled=None)
250 250
251 251 elif isinstance(transformer, MetaHasTraits):
252 252 #Transformer is configurable. Make sure to pass in new default for
253 253 #the enabled flag if one was specified.
254 254 transformer_instance = transformer(parent=self)
255 255 if enabled is not None:
256 256 transformer_instance.enabled = True
257 257
258 258 else:
259 259 #Transformer is not configurable, construct it
260 260 transformer_instance = transformer()
261 261
262 262 #Register and return the transformer.
263 263 self._transformers.append(transformer_instance)
264 264 return transformer_instance
265 265
266 266
267 267 def register_filter(self, name, filter):
268 268 """
269 269 Register a filter.
270 270 A filter is a function that accepts and acts on one string.
271 271 The filters are accesible within the Jinja templating engine.
272 272
273 273 Parameters
274 274 ----------
275 275 name : str
276 276 name to give the filter in the Jinja engine
277 277 filter : filter
278 278 """
279 279 if inspect.isfunction(filter):
280 280 self.environment.filters[name] = filter
281 281 elif isinstance(filter, types.StringTypes):
282 282 filter_cls = import_item(DottedObjectName(filter))
283 283 self.register_filter(name, filter_cls)
284 284 elif isinstance(filter, MetaHasTraits):
285 285 self.environment.filters[name] = filter(config=self.config)
286 286 else:
287 287 self.environment.filters[name] = filter()
288 288 return self.environment.filters[name]
289 289
290 290
291 291 def _init_environment(self, extra_loaders=None):
292 292 """
293 293 Create the Jinja templating environment.
294 294 """
295 295 here = os.path.dirname(os.path.realpath(__file__))
296 296 loaders = []
297 297 if extra_loaders:
298 298 loaders.extend(extra_loaders)
299 299
300 300 loaders.append(FileSystemLoader([
301 301 os.path.join(here, self.template_path),
302 302 os.path.join(here, self.template_skeleton_path),
303 303 ]))
304 304
305 305 self.environment = Environment(
306 306 loader= ChoiceLoader(loaders),
307 307 extensions=JINJA_EXTENSIONS
308 308 )
309 309
310 310 #Set special Jinja2 syntax that will not conflict with latex.
311 311 if self.jinja_logic_block_start:
312 312 self.environment.block_start_string = self.jinja_logic_block_start
313 313 if self.jinja_logic_block_end:
314 314 self.environment.block_end_string = self.jinja_logic_block_end
315 315 if self.jinja_variable_block_start:
316 316 self.environment.variable_start_string = self.jinja_variable_block_start
317 317 if self.jinja_variable_block_end:
318 318 self.environment.variable_end_string = self.jinja_variable_block_end
319 319 if self.jinja_comment_block_start:
320 320 self.environment.comment_start_string = self.jinja_comment_block_start
321 321 if self.jinja_comment_block_end:
322 322 self.environment.comment_end_string = self.jinja_comment_block_end
323 323
324 324
325 325 def _init_transformers(self):
326 326 """
327 327 Register all of the transformers needed for this exporter, disabled
328 328 unless specified explicitly.
329 329 """
330 330 self._transformers = []
331 331
332 332 #Load default transformers (not necessarly enabled by default).
333 333 if self.default_transformers:
334 334 for transformer in self.default_transformers:
335 335 self.register_transformer(transformer)
336 336
337 337 #Load user transformers. Enable by default.
338 338 if self.transformers:
339 339 for transformer in self.transformers:
340 340 self.register_transformer(transformer, enabled=True)
341 341
342 342
343 343 def _init_filters(self):
344 344 """
345 345 Register all of the filters required for the exporter.
346 346 """
347 347
348 348 #Add default filters to the Jinja2 environment
349 349 for key, value in default_filters.iteritems():
350 350 self.register_filter(key, value)
351 351
352 352 #Load user filters. Overwrite existing filters if need be.
353 353 if self.filters:
354 354 for key, user_filter in self.filters.iteritems():
355 355 self.register_filter(key, user_filter)
356 356
357 357
358 358 def _init_resources(self, resources):
359 359
360 360 #Make sure the resources dict is of ResourcesDict type.
361 361 if resources is None:
362 362 resources = ResourcesDict()
363 363 if not isinstance(resources, ResourcesDict):
364 364 new_resources = ResourcesDict()
365 365 new_resources.update(resources)
366 366 resources = new_resources
367 367
368 368 #Make sure the metadata extension exists in resources
369 369 if 'metadata' in resources:
370 370 if not isinstance(resources['metadata'], ResourcesDict):
371 371 resources['metadata'] = ResourcesDict(resources['metadata'])
372 372 else:
373 373 resources['metadata'] = ResourcesDict()
374 resources['metadata']['name'] = 'Notebook'
375 resources['metadata']['modified_date'] = ''
374 if not resources['metadata']['name']:
375 resources['metadata']['name']: = 'Notebook'
376 376
377 377 #Set the output extension
378 378 resources['output_extension'] = self.file_extension
379 379 return resources
380 380
381 381
382 382 def _transform(self, nb, resources):
383 383 """
384 384 Preprocess the notebook before passing it into the Jinja engine.
385 385 To preprocess the notebook is to apply all of the
386 386
387 387 Parameters
388 388 ----------
389 389 nb : notebook node
390 390 notebook that is being exported.
391 391 resources : a dict of additional resources that
392 392 can be accessed read/write by transformers
393 393 and filters.
394 394 """
395 395
396 396 # Do a copy.deepcopy first,
397 397 # we are never safe enough with what the transformers could do.
398 398 nbc = copy.deepcopy(nb)
399 399 resc = copy.deepcopy(resources)
400 400
401 401 #Run each transformer on the notebook. Carry the output along
402 402 #to each transformer
403 403 for transformer in self._transformers:
404 404 nbc, resc = transformer(nbc, resc)
405 405 return nbc, resc
General Comments 0
You need to be logged in to leave comments. Login now