##// END OF EJS Templates
Catch specific error instead of all errors.
Jonathan Frederic -
Show More
@@ -1,482 +1,482 b''
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 copy
24 24 import collections
25 25 import datetime
26 26
27 27 # other libs/dependencies
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader
28 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
29 29
30 30 # IPython imports
31 31 from IPython.config.configurable import Configurable
32 32 from IPython.config import Config
33 33 from IPython.nbformat import current as nbformat
34 34 from IPython.utils.traitlets import MetaHasTraits, DottedObjectName, Unicode, List, Dict
35 35 from IPython.utils.importstring import import_item
36 36 from IPython.utils.text import indent
37 37 from IPython.utils import py3compat
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 'markdown2html': filters.markdown2html,
52 52 'ansi2html': filters.ansi2html,
53 53 'filter_data_type': filters.DataTypeFilter,
54 54 'get_lines': filters.get_lines,
55 55 'highlight2html': filters.highlight2html,
56 56 'highlight2latex': filters.highlight2latex,
57 57 'ipython2python': filters.ipython2python,
58 58 'markdown2latex': filters.markdown2latex,
59 59 'markdown2rst': filters.markdown2rst,
60 60 'comment_lines': filters.comment_lines,
61 61 'strip_ansi': filters.strip_ansi,
62 62 'strip_dollars': filters.strip_dollars,
63 63 'strip_files_prefix': filters.strip_files_prefix,
64 64 'html2text' : filters.html2text,
65 65 'add_anchor': filters.add_anchor,
66 66 'ansi2latex': filters.ansi2latex,
67 67 'strip_math_space': filters.strip_math_space,
68 68 'wrap_text': filters.wrap_text,
69 69 'escape_latex': filters.escape_latex
70 70 }
71 71
72 72 #-----------------------------------------------------------------------------
73 73 # Class
74 74 #-----------------------------------------------------------------------------
75 75
76 76 class ResourcesDict(collections.defaultdict):
77 77 def __missing__(self, key):
78 78 return ''
79 79
80 80
81 81 class Exporter(Configurable):
82 82 """
83 83 Exports notebooks into other file formats. Uses Jinja 2 templating engine
84 84 to output new formats. Inherit from this class if you are creating a new
85 85 template type along with new filters/transformers. If the filters/
86 86 transformers provided by default suffice, there is no need to inherit from
87 87 this class. Instead, override the template_file and file_extension
88 88 traits via a config file.
89 89
90 90 {filters}
91 91 """
92 92
93 93 # finish the docstring
94 94 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
95 95
96 96
97 97 template_file = Unicode(u'default',
98 98 config=True,
99 99 help="Name of the template file to use")
100 100 def _template_file_changed(self, name, old, new):
101 101 if new=='default':
102 102 self.template_file = self.default_template
103 103 else:
104 104 self.template_file = new
105 105 default_template = Unicode(u'')
106 106
107 107 file_extension = Unicode(
108 108 'txt', config=True,
109 109 help="Extension of the file that should be written to disk"
110 110 )
111 111
112 112 template_path = List(['.'], config=True)
113 113
114 114 default_template_path = Unicode(
115 115 os.path.join("..", "templates"),
116 116 help="Path where the template files are located.")
117 117
118 118 template_skeleton_path = Unicode(
119 119 os.path.join("..", "templates", "skeleton"),
120 120 help="Path where the template skeleton files are located.")
121 121
122 122 #Jinja block definitions
123 123 jinja_comment_block_start = Unicode("", config=True)
124 124 jinja_comment_block_end = Unicode("", config=True)
125 125 jinja_variable_block_start = Unicode("", config=True)
126 126 jinja_variable_block_end = Unicode("", config=True)
127 127 jinja_logic_block_start = Unicode("", config=True)
128 128 jinja_logic_block_end = Unicode("", config=True)
129 129
130 130 #Extension that the template files use.
131 131 template_extension = Unicode(".tpl", config=True)
132 132
133 133 #Configurability, allows the user to easily add filters and transformers.
134 134 transformers = List(config=True,
135 135 help="""List of transformers, by name or namespace, to enable.""")
136 136
137 137 filters = Dict(config=True,
138 138 help="""Dictionary of filters, by name and namespace, to add to the Jinja
139 139 environment.""")
140 140
141 141 default_transformers = List([nbtransformers.coalesce_streams,
142 142 nbtransformers.SVG2PDFTransformer,
143 143 nbtransformers.ExtractOutputTransformer,
144 144 nbtransformers.CSSHTMLHeaderTransformer,
145 145 nbtransformers.RevealHelpTransformer,
146 146 nbtransformers.LatexTransformer,
147 147 nbtransformers.SphinxTransformer],
148 148 config=True,
149 149 help="""List of transformers available by default, by name, namespace,
150 150 instance, or type.""")
151 151
152 152
153 153 def __init__(self, config=None, extra_loaders=None, **kw):
154 154 """
155 155 Public constructor
156 156
157 157 Parameters
158 158 ----------
159 159 config : config
160 160 User configuration instance.
161 161 extra_loaders : list[of Jinja Loaders]
162 162 ordered list of Jinja loder to find templates. Will be tried in order
163 163 before the default FileSysteme ones.
164 164 template : str (optional, kw arg)
165 165 Template to use when exporting.
166 166 """
167 167
168 168 #Call the base class constructor
169 169 c = self.default_config
170 170 if config:
171 171 c.merge(config)
172 172
173 173 super(Exporter, self).__init__(config=c, **kw)
174 174
175 175 #Init
176 176 self._init_template(**kw)
177 177 self._init_environment(extra_loaders=extra_loaders)
178 178 self._init_transformers()
179 179 self._init_filters()
180 180
181 181
182 182 @property
183 183 def default_config(self):
184 184 return Config()
185 185
186 186
187 187 def from_notebook_node(self, nb, resources=None, **kw):
188 188 """
189 189 Convert a notebook from a notebook node instance.
190 190
191 191 Parameters
192 192 ----------
193 193 nb : Notebook node
194 194 resources : dict (**kw)
195 195 of additional resources that can be accessed read/write by
196 196 transformers and filters.
197 197 """
198 198 nb_copy = copy.deepcopy(nb)
199 199 resources = self._init_resources(resources)
200 200
201 201 # Preprocess
202 202 nb_copy, resources = self._transform(nb_copy, resources)
203 203
204 204 # Try different template names during conversion. First try to load the
205 205 # template by name with extension added, then try loading the template
206 206 # as if the name is explicitly specified, then try the name as a
207 207 # 'flavor', and lastly just try to load the template by module name.
208 208 module_name = self.__module__.split('.')[-1]
209 209 try_names = [self.template_file + self.template_extension,
210 210 self.template_file,
211 211 module_name + '_' + self.template_file + self.template_extension,
212 212 module_name + self.template_extension]
213 213 for try_name in try_names:
214 214 try:
215 215 self.template = self.environment.get_template(try_name)
216 216 break
217 except:
217 except TemplateNotFound:
218 218 pass
219 219
220 220 if hasattr(self, 'template'):
221 221 output = self.template.render(nb=nb_copy, resources=resources)
222 222 else:
223 223 raise IOError('template file "%s" could not be found' % self.template_file)
224 224 return output, resources
225 225
226 226
227 227 def from_filename(self, filename, resources=None, **kw):
228 228 """
229 229 Convert a notebook from a notebook file.
230 230
231 231 Parameters
232 232 ----------
233 233 filename : str
234 234 Full filename of the notebook file to open and convert.
235 235 """
236 236
237 237 #Pull the metadata from the filesystem.
238 238 if resources is None:
239 239 resources = ResourcesDict()
240 240 if not 'metadata' in resources or resources['metadata'] == '':
241 241 resources['metadata'] = ResourcesDict()
242 242 basename = os.path.basename(filename)
243 243 notebook_name = basename[:basename.rfind('.')]
244 244 resources['metadata']['name'] = notebook_name
245 245
246 246 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
247 247 resources['metadata']['modified_date'] = modified_date.strftime("%B %-d, %Y")
248 248
249 249 with io.open(filename) as f:
250 250 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
251 251
252 252
253 253 def from_file(self, file_stream, resources=None, **kw):
254 254 """
255 255 Convert a notebook from a notebook file.
256 256
257 257 Parameters
258 258 ----------
259 259 file_stream : file-like object
260 260 Notebook file-like object to convert.
261 261 """
262 262 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
263 263
264 264
265 265 def register_transformer(self, transformer, enabled=False):
266 266 """
267 267 Register a transformer.
268 268 Transformers are classes that act upon the notebook before it is
269 269 passed into the Jinja templating engine. Transformers are also
270 270 capable of passing additional information to the Jinja
271 271 templating engine.
272 272
273 273 Parameters
274 274 ----------
275 275 transformer : transformer
276 276 """
277 277 if transformer is None:
278 278 raise TypeError('transformer')
279 279 isclass = isinstance(transformer, type)
280 280 constructed = not isclass
281 281
282 282 #Handle transformer's registration based on it's type
283 283 if constructed and isinstance(transformer, py3compat.string_types):
284 284 #Transformer is a string, import the namespace and recursively call
285 285 #this register_transformer method
286 286 transformer_cls = import_item(transformer)
287 287 return self.register_transformer(transformer_cls, enabled)
288 288
289 289 if constructed and hasattr(transformer, '__call__'):
290 290 #Transformer is a function, no need to construct it.
291 291 #Register and return the transformer.
292 292 if enabled:
293 293 transformer.enabled = True
294 294 self._transformers.append(transformer)
295 295 return transformer
296 296
297 297 elif isclass and isinstance(transformer, MetaHasTraits):
298 298 #Transformer is configurable. Make sure to pass in new default for
299 299 #the enabled flag if one was specified.
300 300 self.register_transformer(transformer(parent=self), enabled)
301 301
302 302 elif isclass:
303 303 #Transformer is not configurable, construct it
304 304 self.register_transformer(transformer(), enabled)
305 305
306 306 else:
307 307 #Transformer is an instance of something without a __call__
308 308 #attribute.
309 309 raise TypeError('transformer')
310 310
311 311
312 312 def register_filter(self, name, jinja_filter):
313 313 """
314 314 Register a filter.
315 315 A filter is a function that accepts and acts on one string.
316 316 The filters are accesible within the Jinja templating engine.
317 317
318 318 Parameters
319 319 ----------
320 320 name : str
321 321 name to give the filter in the Jinja engine
322 322 filter : filter
323 323 """
324 324 if jinja_filter is None:
325 325 raise TypeError('filter')
326 326 isclass = isinstance(jinja_filter, type)
327 327 constructed = not isclass
328 328
329 329 #Handle filter's registration based on it's type
330 330 if constructed and isinstance(jinja_filter, py3compat.string_types):
331 331 #filter is a string, import the namespace and recursively call
332 332 #this register_filter method
333 333 filter_cls = import_item(jinja_filter)
334 334 return self.register_filter(name, filter_cls)
335 335
336 336 if constructed and hasattr(jinja_filter, '__call__'):
337 337 #filter is a function, no need to construct it.
338 338 self.environment.filters[name] = jinja_filter
339 339 return jinja_filter
340 340
341 341 elif isclass and isinstance(jinja_filter, MetaHasTraits):
342 342 #filter is configurable. Make sure to pass in new default for
343 343 #the enabled flag if one was specified.
344 344 filter_instance = jinja_filter(parent=self)
345 345 self.register_filter(name, filter_instance )
346 346
347 347 elif isclass:
348 348 #filter is not configurable, construct it
349 349 filter_instance = jinja_filter()
350 350 self.register_filter(name, filter_instance)
351 351
352 352 else:
353 353 #filter is an instance of something without a __call__
354 354 #attribute.
355 355 raise TypeError('filter')
356 356
357 357
358 358 def _init_template(self, **kw):
359 359 """
360 360 Make sure a template name is specified. If one isn't specified, try to
361 361 build one from the information we know.
362 362 """
363 363 self._template_file_changed('template_file', self.template_file, self.template_file)
364 364 if 'template' in kw:
365 365 self.template_file = kw['template']
366 366
367 367
368 368 def _init_environment(self, extra_loaders=None):
369 369 """
370 370 Create the Jinja templating environment.
371 371 """
372 372 here = os.path.dirname(os.path.realpath(__file__))
373 373 loaders = []
374 374 if extra_loaders:
375 375 loaders.extend(extra_loaders)
376 376
377 377 paths = self.template_path
378 378 paths.extend([os.path.join(here, self.default_template_path),
379 379 os.path.join(here, self.template_skeleton_path)])
380 380 loaders.append(FileSystemLoader(paths))
381 381
382 382 self.environment = Environment(
383 383 loader= ChoiceLoader(loaders),
384 384 extensions=JINJA_EXTENSIONS
385 385 )
386 386
387 387 #Set special Jinja2 syntax that will not conflict with latex.
388 388 if self.jinja_logic_block_start:
389 389 self.environment.block_start_string = self.jinja_logic_block_start
390 390 if self.jinja_logic_block_end:
391 391 self.environment.block_end_string = self.jinja_logic_block_end
392 392 if self.jinja_variable_block_start:
393 393 self.environment.variable_start_string = self.jinja_variable_block_start
394 394 if self.jinja_variable_block_end:
395 395 self.environment.variable_end_string = self.jinja_variable_block_end
396 396 if self.jinja_comment_block_start:
397 397 self.environment.comment_start_string = self.jinja_comment_block_start
398 398 if self.jinja_comment_block_end:
399 399 self.environment.comment_end_string = self.jinja_comment_block_end
400 400
401 401
402 402 def _init_transformers(self):
403 403 """
404 404 Register all of the transformers needed for this exporter, disabled
405 405 unless specified explicitly.
406 406 """
407 407 self._transformers = []
408 408
409 409 #Load default transformers (not necessarly enabled by default).
410 410 if self.default_transformers:
411 411 for transformer in self.default_transformers:
412 412 self.register_transformer(transformer)
413 413
414 414 #Load user transformers. Enable by default.
415 415 if self.transformers:
416 416 for transformer in self.transformers:
417 417 self.register_transformer(transformer, enabled=True)
418 418
419 419
420 420 def _init_filters(self):
421 421 """
422 422 Register all of the filters required for the exporter.
423 423 """
424 424
425 425 #Add default filters to the Jinja2 environment
426 426 for key, value in default_filters.items():
427 427 self.register_filter(key, value)
428 428
429 429 #Load user filters. Overwrite existing filters if need be.
430 430 if self.filters:
431 431 for key, user_filter in self.filters.items():
432 432 self.register_filter(key, user_filter)
433 433
434 434
435 435 def _init_resources(self, resources):
436 436
437 437 #Make sure the resources dict is of ResourcesDict type.
438 438 if resources is None:
439 439 resources = ResourcesDict()
440 440 if not isinstance(resources, ResourcesDict):
441 441 new_resources = ResourcesDict()
442 442 new_resources.update(resources)
443 443 resources = new_resources
444 444
445 445 #Make sure the metadata extension exists in resources
446 446 if 'metadata' in resources:
447 447 if not isinstance(resources['metadata'], ResourcesDict):
448 448 resources['metadata'] = ResourcesDict(resources['metadata'])
449 449 else:
450 450 resources['metadata'] = ResourcesDict()
451 451 if not resources['metadata']['name']:
452 452 resources['metadata']['name'] = 'Notebook'
453 453
454 454 #Set the output extension
455 455 resources['output_extension'] = self.file_extension
456 456 return resources
457 457
458 458
459 459 def _transform(self, nb, resources):
460 460 """
461 461 Preprocess the notebook before passing it into the Jinja engine.
462 462 To preprocess the notebook is to apply all of the
463 463
464 464 Parameters
465 465 ----------
466 466 nb : notebook node
467 467 notebook that is being exported.
468 468 resources : a dict of additional resources that
469 469 can be accessed read/write by transformers
470 470 and filters.
471 471 """
472 472
473 473 # Do a copy.deepcopy first,
474 474 # we are never safe enough with what the transformers could do.
475 475 nbc = copy.deepcopy(nb)
476 476 resc = copy.deepcopy(resources)
477 477
478 478 #Run each transformer on the notebook. Carry the output along
479 479 #to each transformer
480 480 for transformer in self._transformers:
481 481 nbc, resc = transformer(nbc, resc)
482 482 return nbc, resc
General Comments 0
You need to be logged in to leave comments. Login now