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