##// END OF EJS Templates
Fixed logic for register filter and transformer
Jonathan Frederic -
Show More
@@ -1,407 +1,433 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 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 resources is None:
202 202 resources = ResourcesDict()
203 203 if not 'metadata' in resources or resources['metadata'] == '':
204 204 resources['metadata'] = ResourcesDict()
205 205 basename = os.path.basename(filename)
206 206 notebook_name = basename[:basename.rfind('.')]
207 207 resources['metadata']['name'] = notebook_name
208 208
209 209 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
210 210 resources['metadata']['modified_date'] = modified_date.strftime("%B %-d, %Y")
211 211
212 212 with io.open(filename) as f:
213 213 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
214 214
215 215
216 216 def from_file(self, file_stream, resources=None, **kw):
217 217 """
218 218 Convert a notebook from a notebook file.
219 219
220 220 Parameters
221 221 ----------
222 222 file_stream : file-like object
223 223 Notebook file-like object to convert.
224 224 """
225 225 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
226 226
227 227
228 def register_transformer(self, transformer, enabled=None):
228 def register_transformer(self, transformer, enabled=False):
229 229 """
230 230 Register a transformer.
231 231 Transformers are classes that act upon the notebook before it is
232 232 passed into the Jinja templating engine. Transformers are also
233 233 capable of passing additional information to the Jinja
234 234 templating engine.
235 235
236 236 Parameters
237 237 ----------
238 238 transformer : transformer
239 239 """
240 if transformer is None:
241 raise TypeError('transformer')
242 isclass = inspect.isclass(transformer)
243 constructed = not isclass
240 244
241 245 #Handle transformer's registration based on it's type
242 if inspect.isfunction(transformer):
246 if constructed and isinstance(transformer, types.StringTypes):
247 #Transformer is a string, import the namespace and recursively call
248 #this register_transformer method
249 transformer_cls = import_item(transformer)
250 return self.register_transformer(transformer_cls, enabled)
251
252 if constructed and hasattr(transformer, '__call__'):
243 253 #Transformer is a function, no need to construct it.
254 #Register and return the transformer.
255 if enabled:
256 transformer.enabled = True
244 257 self._transformers.append(transformer)
245 258 return transformer
246 259
247 elif isinstance(transformer, types.StringTypes):
248 #Transformer is a string, import the namespace and recursively call
249 #this register_transformer method
250 transformer_cls = import_item(DottedObjectName(transformer))
251 return self.register_transformer(transformer_cls, enabled=None)
252
253 elif isinstance(transformer, MetaHasTraits):
260 elif isclass and isinstance(transformer, MetaHasTraits):
254 261 #Transformer is configurable. Make sure to pass in new default for
255 262 #the enabled flag if one was specified.
256 transformer_instance = transformer(parent=self)
257 if enabled is not None:
258 transformer_instance.enabled = True
263 self.register_transformer(transformer(parent=self), enabled)
259 264
260 else:
265 elif isclass:
261 266 #Transformer is not configurable, construct it
262 transformer_instance = transformer()
267 self.register_transformer(transformer(), enabled)
263 268
264 #Register and return the transformer.
265 self._transformers.append(transformer_instance)
266 return transformer_instance
269 else:
270 #Transformer is an instance of something without a __call__
271 #attribute.
272 raise TypeError('transformer')
267 273
268 274
269 275 def register_filter(self, name, filter):
270 276 """
271 277 Register a filter.
272 278 A filter is a function that accepts and acts on one string.
273 279 The filters are accesible within the Jinja templating engine.
274 280
275 281 Parameters
276 282 ----------
277 283 name : str
278 284 name to give the filter in the Jinja engine
279 285 filter : filter
280 286 """
281 if inspect.isfunction(filter):
287 if filter is None:
288 raise TypeError('filter')
289 isclass = inspect.isclass(filter)
290 constructed = not isclass
291
292 #Handle filter's registration based on it's type
293 if constructed and isinstance(filter, types.StringTypes):
294 #filter is a string, import the namespace and recursively call
295 #this register_filter method
296 filter_cls = import_item(filter)
297 return self.register_filter(name, filter_cls)
298
299 if constructed and hasattr(filter, '__call__'):
300 #filter is a function, no need to construct it.
282 301 self.environment.filters[name] = filter
283 elif isinstance(filter, types.StringTypes):
284 filter_cls = import_item(DottedObjectName(filter))
285 self.register_filter(name, filter_cls)
286 elif isinstance(filter, MetaHasTraits):
287 self.environment.filters[name] = filter(config=self.config)
302 return filter
303
304 elif isclass and isinstance(filter, MetaHasTraits):
305 #filter is configurable. Make sure to pass in new default for
306 #the enabled flag if one was specified.
307 self.register_filter(name, filter(parent=self))
308
309 elif isclass:
310 #filter is not configurable, construct it
311 self.register_filter(name, filter())
312
288 313 else:
289 self.environment.filters[name] = filter()
290 return self.environment.filters[name]
314 #filter is an instance of something without a __call__
315 #attribute.
316 raise TypeError('filter')
291 317
292 318
293 319 def _init_environment(self, extra_loaders=None):
294 320 """
295 321 Create the Jinja templating environment.
296 322 """
297 323 here = os.path.dirname(os.path.realpath(__file__))
298 324 loaders = []
299 325 if extra_loaders:
300 326 loaders.extend(extra_loaders)
301 327
302 328 loaders.append(FileSystemLoader([
303 329 os.path.join(here, self.template_path),
304 330 os.path.join(here, self.template_skeleton_path),
305 331 ]))
306 332
307 333 self.environment = Environment(
308 334 loader= ChoiceLoader(loaders),
309 335 extensions=JINJA_EXTENSIONS
310 336 )
311 337
312 338 #Set special Jinja2 syntax that will not conflict with latex.
313 339 if self.jinja_logic_block_start:
314 340 self.environment.block_start_string = self.jinja_logic_block_start
315 341 if self.jinja_logic_block_end:
316 342 self.environment.block_end_string = self.jinja_logic_block_end
317 343 if self.jinja_variable_block_start:
318 344 self.environment.variable_start_string = self.jinja_variable_block_start
319 345 if self.jinja_variable_block_end:
320 346 self.environment.variable_end_string = self.jinja_variable_block_end
321 347 if self.jinja_comment_block_start:
322 348 self.environment.comment_start_string = self.jinja_comment_block_start
323 349 if self.jinja_comment_block_end:
324 350 self.environment.comment_end_string = self.jinja_comment_block_end
325 351
326 352
327 353 def _init_transformers(self):
328 354 """
329 355 Register all of the transformers needed for this exporter, disabled
330 356 unless specified explicitly.
331 357 """
332 358 self._transformers = []
333 359
334 360 #Load default transformers (not necessarly enabled by default).
335 361 if self.default_transformers:
336 362 for transformer in self.default_transformers:
337 363 self.register_transformer(transformer)
338 364
339 365 #Load user transformers. Enable by default.
340 366 if self.transformers:
341 367 for transformer in self.transformers:
342 368 self.register_transformer(transformer, enabled=True)
343 369
344 370
345 371 def _init_filters(self):
346 372 """
347 373 Register all of the filters required for the exporter.
348 374 """
349 375
350 376 #Add default filters to the Jinja2 environment
351 377 for key, value in default_filters.iteritems():
352 378 self.register_filter(key, value)
353 379
354 380 #Load user filters. Overwrite existing filters if need be.
355 381 if self.filters:
356 382 for key, user_filter in self.filters.iteritems():
357 383 self.register_filter(key, user_filter)
358 384
359 385
360 386 def _init_resources(self, resources):
361 387
362 388 #Make sure the resources dict is of ResourcesDict type.
363 389 if resources is None:
364 390 resources = ResourcesDict()
365 391 if not isinstance(resources, ResourcesDict):
366 392 new_resources = ResourcesDict()
367 393 new_resources.update(resources)
368 394 resources = new_resources
369 395
370 396 #Make sure the metadata extension exists in resources
371 397 if 'metadata' in resources:
372 398 if not isinstance(resources['metadata'], ResourcesDict):
373 399 resources['metadata'] = ResourcesDict(resources['metadata'])
374 400 else:
375 401 resources['metadata'] = ResourcesDict()
376 402 if not resources['metadata']['name']:
377 403 resources['metadata']['name'] = 'Notebook'
378 404
379 405 #Set the output extension
380 406 resources['output_extension'] = self.file_extension
381 407 return resources
382 408
383 409
384 410 def _transform(self, nb, resources):
385 411 """
386 412 Preprocess the notebook before passing it into the Jinja engine.
387 413 To preprocess the notebook is to apply all of the
388 414
389 415 Parameters
390 416 ----------
391 417 nb : notebook node
392 418 notebook that is being exported.
393 419 resources : a dict of additional resources that
394 420 can be accessed read/write by transformers
395 421 and filters.
396 422 """
397 423
398 424 # Do a copy.deepcopy first,
399 425 # we are never safe enough with what the transformers could do.
400 426 nbc = copy.deepcopy(nb)
401 427 resc = copy.deepcopy(resources)
402 428
403 429 #Run each transformer on the notebook. Carry the output along
404 430 #to each transformer
405 431 for transformer in self._transformers:
406 432 nbc, resc = transformer(nbc, resc)
407 433 return nbc, resc
General Comments 0
You need to be logged in to leave comments. Login now