##// END OF EJS Templates
Merge pull request #3749 from minrk/pytransform...
Matthias Bussonnier -
r11720:14c3b91d merge
parent child Browse files
Show More
@@ -1,445 +1,446 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
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 'ipython2python': filters.ipython2python,
57 58 'markdown2latex': filters.markdown2latex,
58 59 'markdown2rst': filters.markdown2rst,
59 60 'comment_lines': filters.comment_lines,
60 61 'strip_ansi': filters.strip_ansi,
61 62 'strip_dollars': filters.strip_dollars,
62 63 'strip_files_prefix': filters.strip_files_prefix,
63 64 'html2text' : filters.html2text,
64 65 'add_anchor': filters.add_anchor,
65 66 'ansi2latex': filters.ansi2latex,
66 67 'strip_math_space': filters.strip_math_space,
67 68 'wrap_text': filters.wrap_text,
68 69 'escape_latex': filters.escape_latex
69 70 }
70 71
71 72 #-----------------------------------------------------------------------------
72 73 # Class
73 74 #-----------------------------------------------------------------------------
74 75
75 76 class ResourcesDict(collections.defaultdict):
76 77 def __missing__(self, key):
77 78 return ''
78 79
79 80
80 81 class Exporter(Configurable):
81 82 """
82 83 Exports notebooks into other file formats. Uses Jinja 2 templating engine
83 84 to output new formats. Inherit from this class if you are creating a new
84 85 template type along with new filters/transformers. If the filters/
85 86 transformers provided by default suffice, there is no need to inherit from
86 87 this class. Instead, override the template_file and file_extension
87 88 traits via a config file.
88 89
89 90 {filters}
90 91 """
91 92
92 93 # finish the docstring
93 94 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
94 95
95 96
96 97 template_file = Unicode(
97 98 '', config=True,
98 99 help="Name of the template file to use")
99 100
100 101 file_extension = Unicode(
101 102 'txt', config=True,
102 103 help="Extension of the file that should be written to disk"
103 104 )
104 105
105 106 template_path = List(['.'], config=True)
106 107
107 108 default_template_path = Unicode(
108 109 os.path.join("..", "templates"),
109 110 help="Path where the template files are located.")
110 111
111 112 template_skeleton_path = Unicode(
112 113 os.path.join("..", "templates", "skeleton"),
113 114 help="Path where the template skeleton files are located.")
114 115
115 116 #Jinja block definitions
116 117 jinja_comment_block_start = Unicode("", config=True)
117 118 jinja_comment_block_end = Unicode("", config=True)
118 119 jinja_variable_block_start = Unicode("", config=True)
119 120 jinja_variable_block_end = Unicode("", config=True)
120 121 jinja_logic_block_start = Unicode("", config=True)
121 122 jinja_logic_block_end = Unicode("", config=True)
122 123
123 124 #Extension that the template files use.
124 125 template_extension = Unicode(".tpl", config=True)
125 126
126 127 #Configurability, allows the user to easily add filters and transformers.
127 128 transformers = List(config=True,
128 129 help="""List of transformers, by name or namespace, to enable.""")
129 130
130 131 filters = Dict(config=True,
131 132 help="""Dictionary of filters, by name and namespace, to add to the Jinja
132 133 environment.""")
133 134
134 135 default_transformers = List([nbtransformers.coalesce_streams,
135 136 nbtransformers.SVG2PDFTransformer,
136 137 nbtransformers.ExtractOutputTransformer,
137 138 nbtransformers.CSSHTMLHeaderTransformer,
138 139 nbtransformers.RevealHelpTransformer,
139 140 nbtransformers.LatexTransformer,
140 141 nbtransformers.SphinxTransformer],
141 142 config=True,
142 143 help="""List of transformers available by default, by name, namespace,
143 144 instance, or type.""")
144 145
145 146
146 147 def __init__(self, config=None, extra_loaders=None, **kw):
147 148 """
148 149 Public constructor
149 150
150 151 Parameters
151 152 ----------
152 153 config : config
153 154 User configuration instance.
154 155 extra_loaders : list[of Jinja Loaders]
155 156 ordered list of Jinja loder to find templates. Will be tried in order
156 157 before the default FileSysteme ones.
157 158 """
158 159
159 160 #Call the base class constructor
160 161 c = self.default_config
161 162 if config:
162 163 c.merge(config)
163 164
164 165 super(Exporter, self).__init__(config=c, **kw)
165 166
166 167 #Init
167 168 self._init_environment(extra_loaders=extra_loaders)
168 169 self._init_transformers()
169 170 self._init_filters()
170 171
171 172
172 173 @property
173 174 def default_config(self):
174 175 return Config()
175 176
176 177
177 178 def from_notebook_node(self, nb, resources=None, **kw):
178 179 """
179 180 Convert a notebook from a notebook node instance.
180 181
181 182 Parameters
182 183 ----------
183 184 nb : Notebook node
184 185 resources : dict (**kw)
185 186 of additional resources that can be accessed read/write by
186 187 transformers and filters.
187 188 """
188 189 nb_copy = copy.deepcopy(nb)
189 190 resources = self._init_resources(resources)
190 191
191 192 #Preprocess
192 193 nb_copy, resources = self._transform(nb_copy, resources)
193 194
194 195 #Convert
195 196 self.template = self.environment.get_template(self.template_file + self.template_extension)
196 197 output = self.template.render(nb=nb_copy, resources=resources)
197 198 return output, resources
198 199
199 200
200 201 def from_filename(self, filename, resources=None, **kw):
201 202 """
202 203 Convert a notebook from a notebook file.
203 204
204 205 Parameters
205 206 ----------
206 207 filename : str
207 208 Full filename of the notebook file to open and convert.
208 209 """
209 210
210 211 #Pull the metadata from the filesystem.
211 212 if resources is None:
212 213 resources = ResourcesDict()
213 214 if not 'metadata' in resources or resources['metadata'] == '':
214 215 resources['metadata'] = ResourcesDict()
215 216 basename = os.path.basename(filename)
216 217 notebook_name = basename[:basename.rfind('.')]
217 218 resources['metadata']['name'] = notebook_name
218 219
219 220 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
220 221 resources['metadata']['modified_date'] = modified_date.strftime("%B %-d, %Y")
221 222
222 223 with io.open(filename) as f:
223 224 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources,**kw)
224 225
225 226
226 227 def from_file(self, file_stream, resources=None, **kw):
227 228 """
228 229 Convert a notebook from a notebook file.
229 230
230 231 Parameters
231 232 ----------
232 233 file_stream : file-like object
233 234 Notebook file-like object to convert.
234 235 """
235 236 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
236 237
237 238
238 239 def register_transformer(self, transformer, enabled=False):
239 240 """
240 241 Register a transformer.
241 242 Transformers are classes that act upon the notebook before it is
242 243 passed into the Jinja templating engine. Transformers are also
243 244 capable of passing additional information to the Jinja
244 245 templating engine.
245 246
246 247 Parameters
247 248 ----------
248 249 transformer : transformer
249 250 """
250 251 if transformer is None:
251 252 raise TypeError('transformer')
252 253 isclass = isinstance(transformer, type)
253 254 constructed = not isclass
254 255
255 256 #Handle transformer's registration based on it's type
256 257 if constructed and isinstance(transformer, py3compat.string_types):
257 258 #Transformer is a string, import the namespace and recursively call
258 259 #this register_transformer method
259 260 transformer_cls = import_item(transformer)
260 261 return self.register_transformer(transformer_cls, enabled)
261 262
262 263 if constructed and hasattr(transformer, '__call__'):
263 264 #Transformer is a function, no need to construct it.
264 265 #Register and return the transformer.
265 266 if enabled:
266 267 transformer.enabled = True
267 268 self._transformers.append(transformer)
268 269 return transformer
269 270
270 271 elif isclass and isinstance(transformer, MetaHasTraits):
271 272 #Transformer is configurable. Make sure to pass in new default for
272 273 #the enabled flag if one was specified.
273 274 self.register_transformer(transformer(parent=self), enabled)
274 275
275 276 elif isclass:
276 277 #Transformer is not configurable, construct it
277 278 self.register_transformer(transformer(), enabled)
278 279
279 280 else:
280 281 #Transformer is an instance of something without a __call__
281 282 #attribute.
282 283 raise TypeError('transformer')
283 284
284 285
285 286 def register_filter(self, name, jinja_filter):
286 287 """
287 288 Register a filter.
288 289 A filter is a function that accepts and acts on one string.
289 290 The filters are accesible within the Jinja templating engine.
290 291
291 292 Parameters
292 293 ----------
293 294 name : str
294 295 name to give the filter in the Jinja engine
295 296 filter : filter
296 297 """
297 298 if jinja_filter is None:
298 299 raise TypeError('filter')
299 300 isclass = isinstance(jinja_filter, type)
300 301 constructed = not isclass
301 302
302 303 #Handle filter's registration based on it's type
303 304 if constructed and isinstance(jinja_filter, py3compat.string_types):
304 305 #filter is a string, import the namespace and recursively call
305 306 #this register_filter method
306 307 filter_cls = import_item(jinja_filter)
307 308 return self.register_filter(name, filter_cls)
308 309
309 310 if constructed and hasattr(jinja_filter, '__call__'):
310 311 #filter is a function, no need to construct it.
311 312 self.environment.filters[name] = jinja_filter
312 313 return jinja_filter
313 314
314 315 elif isclass and isinstance(jinja_filter, MetaHasTraits):
315 316 #filter is configurable. Make sure to pass in new default for
316 317 #the enabled flag if one was specified.
317 318 filter_instance = jinja_filter(parent=self)
318 319 self.register_filter(name, filter_instance )
319 320
320 321 elif isclass:
321 322 #filter is not configurable, construct it
322 323 filter_instance = jinja_filter()
323 324 self.register_filter(name, filter_instance)
324 325
325 326 else:
326 327 #filter is an instance of something without a __call__
327 328 #attribute.
328 329 raise TypeError('filter')
329 330
330 331
331 332 def _init_environment(self, extra_loaders=None):
332 333 """
333 334 Create the Jinja templating environment.
334 335 """
335 336 here = os.path.dirname(os.path.realpath(__file__))
336 337 loaders = []
337 338 if extra_loaders:
338 339 loaders.extend(extra_loaders)
339 340
340 341 paths = self.template_path
341 342 paths.extend([os.path.join(here, self.default_template_path),
342 343 os.path.join(here, self.template_skeleton_path)])
343 344 loaders.append(FileSystemLoader(paths))
344 345
345 346 self.environment = Environment(
346 347 loader= ChoiceLoader(loaders),
347 348 extensions=JINJA_EXTENSIONS
348 349 )
349 350
350 351 #Set special Jinja2 syntax that will not conflict with latex.
351 352 if self.jinja_logic_block_start:
352 353 self.environment.block_start_string = self.jinja_logic_block_start
353 354 if self.jinja_logic_block_end:
354 355 self.environment.block_end_string = self.jinja_logic_block_end
355 356 if self.jinja_variable_block_start:
356 357 self.environment.variable_start_string = self.jinja_variable_block_start
357 358 if self.jinja_variable_block_end:
358 359 self.environment.variable_end_string = self.jinja_variable_block_end
359 360 if self.jinja_comment_block_start:
360 361 self.environment.comment_start_string = self.jinja_comment_block_start
361 362 if self.jinja_comment_block_end:
362 363 self.environment.comment_end_string = self.jinja_comment_block_end
363 364
364 365
365 366 def _init_transformers(self):
366 367 """
367 368 Register all of the transformers needed for this exporter, disabled
368 369 unless specified explicitly.
369 370 """
370 371 self._transformers = []
371 372
372 373 #Load default transformers (not necessarly enabled by default).
373 374 if self.default_transformers:
374 375 for transformer in self.default_transformers:
375 376 self.register_transformer(transformer)
376 377
377 378 #Load user transformers. Enable by default.
378 379 if self.transformers:
379 380 for transformer in self.transformers:
380 381 self.register_transformer(transformer, enabled=True)
381 382
382 383
383 384 def _init_filters(self):
384 385 """
385 386 Register all of the filters required for the exporter.
386 387 """
387 388
388 389 #Add default filters to the Jinja2 environment
389 390 for key, value in default_filters.items():
390 391 self.register_filter(key, value)
391 392
392 393 #Load user filters. Overwrite existing filters if need be.
393 394 if self.filters:
394 395 for key, user_filter in self.filters.items():
395 396 self.register_filter(key, user_filter)
396 397
397 398
398 399 def _init_resources(self, resources):
399 400
400 401 #Make sure the resources dict is of ResourcesDict type.
401 402 if resources is None:
402 403 resources = ResourcesDict()
403 404 if not isinstance(resources, ResourcesDict):
404 405 new_resources = ResourcesDict()
405 406 new_resources.update(resources)
406 407 resources = new_resources
407 408
408 409 #Make sure the metadata extension exists in resources
409 410 if 'metadata' in resources:
410 411 if not isinstance(resources['metadata'], ResourcesDict):
411 412 resources['metadata'] = ResourcesDict(resources['metadata'])
412 413 else:
413 414 resources['metadata'] = ResourcesDict()
414 415 if not resources['metadata']['name']:
415 416 resources['metadata']['name'] = 'Notebook'
416 417
417 418 #Set the output extension
418 419 resources['output_extension'] = self.file_extension
419 420 return resources
420 421
421 422
422 423 def _transform(self, nb, resources):
423 424 """
424 425 Preprocess the notebook before passing it into the Jinja engine.
425 426 To preprocess the notebook is to apply all of the
426 427
427 428 Parameters
428 429 ----------
429 430 nb : notebook node
430 431 notebook that is being exported.
431 432 resources : a dict of additional resources that
432 433 can be accessed read/write by transformers
433 434 and filters.
434 435 """
435 436
436 437 # Do a copy.deepcopy first,
437 438 # we are never safe enough with what the transformers could do.
438 439 nbc = copy.deepcopy(nb)
439 440 resc = copy.deepcopy(resources)
440 441
441 442 #Run each transformer on the notebook. Carry the output along
442 443 #to each transformer
443 444 for transformer in self._transformers:
444 445 nbc, resc = transformer(nbc, resc)
445 446 return nbc, resc
@@ -1,152 +1,167 b''
1 1 # coding: utf-8
2 2 """String filters.
3 3
4 4 Contains a collection of useful string manipulation filters for use in Jinja
5 5 templates.
6 6 """
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (c) 2013, the IPython Development Team.
9 9 #
10 10 # Distributed under the terms of the Modified BSD License.
11 11 #
12 12 # The full license is in the file COPYING.txt, distributed with this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import re
20 20 import textwrap
21 21 from xml.etree import ElementTree
22
23 from IPython.core.interactiveshell import InteractiveShell
22 24 from IPython.utils import py3compat
23 25
24 26 #-----------------------------------------------------------------------------
25 27 # Functions
26 28 #-----------------------------------------------------------------------------
27 29
28 30 __all__ = [
29 31 'wrap_text',
30 32 'html2text',
31 33 'add_anchor',
32 34 'strip_dollars',
33 35 'strip_files_prefix',
34 36 'comment_lines',
35 'get_lines'
37 'get_lines',
38 'ipython2python',
36 39 ]
37 40
38 41
39 42 def wrap_text(text, width=100):
40 43 """
41 44 Intelligently wrap text.
42 45 Wrap text without breaking words if possible.
43 46
44 47 Parameters
45 48 ----------
46 49 text : str
47 50 Text to wrap.
48 51 width : int, optional
49 52 Number of characters to wrap to, default 100.
50 53 """
51 54
52 55 split_text = text.split('\n')
53 56 wrp = map(lambda x:textwrap.wrap(x,width), split_text)
54 57 wrpd = map('\n'.join, wrp)
55 58 return '\n'.join(wrpd)
56 59
57 60
58 61 def html2text(element):
59 62 """extract inner text from html
60 63
61 64 Analog of jQuery's $(element).text()
62 65 """
63 66 if isinstance(element, py3compat.string_types):
64 67 element = ElementTree.fromstring(element)
65 68
66 69 text = element.text or ""
67 70 for child in element:
68 71 text += html2text(child)
69 72 text += (element.tail or "")
70 73 return text
71 74
72 75
73 76 def add_anchor(html):
74 77 """Add an anchor-link to an html header tag
75 78
76 79 For use in heading cells
77 80 """
78 81 h = ElementTree.fromstring(py3compat.cast_bytes_py2(html))
79 82 link = html2text(h).replace(' ', '-')
80 83 h.set('id', link)
81 84 a = ElementTree.Element("a", {"class" : "anchor-link", "href" : "#" + link})
82 85 a.text = u'ΒΆ'
83 86 h.append(a)
84 87 return ElementTree.tostring(h)
85 88
86 89
87 90 def strip_dollars(text):
88 91 """
89 92 Remove all dollar symbols from text
90 93
91 94 Parameters
92 95 ----------
93 96 text : str
94 97 Text to remove dollars from
95 98 """
96 99
97 100 return text.strip('$')
98 101
99 102
100 103 files_url_pattern = re.compile(r'(src|href)\=([\'"]?)files/')
101 104
102 105 def strip_files_prefix(text):
103 106 """
104 107 Fix all fake URLs that start with `files/`,
105 108 stripping out the `files/` prefix.
106 109
107 110 Parameters
108 111 ----------
109 112 text : str
110 113 Text in which to replace 'src="files/real...' with 'src="real...'
111 114 """
112 115 return files_url_pattern.sub(r"\1=\2", text)
113 116
114 117
115 118 def comment_lines(text, prefix='# '):
116 119 """
117 120 Build a Python comment line from input text.
118 121
119 122 Parameters
120 123 ----------
121 124 text : str
122 125 Text to comment out.
123 126 prefix : str
124 127 Character to append to the start of each line.
125 128 """
126 129
127 130 #Replace line breaks with line breaks and comment symbols.
128 131 #Also add a comment symbol at the beginning to comment out
129 132 #the first line.
130 133 return prefix + ('\n'+prefix).join(text.split('\n'))
131 134
132 135
133 136 def get_lines(text, start=None,end=None):
134 137 """
135 138 Split the input text into separate lines and then return the
136 139 lines that the caller is interested in.
137 140
138 141 Parameters
139 142 ----------
140 143 text : str
141 144 Text to parse lines from.
142 145 start : int, optional
143 146 First line to grab from.
144 147 end : int, optional
145 148 Last line to grab from.
146 149 """
147 150
148 151 # Split the input into lines.
149 152 lines = text.split("\n")
150 153
151 154 # Return the right lines.
152 155 return "\n".join(lines[start:end]) #re-join
156
157 def ipython2python(code):
158 """Transform IPython syntax to pure Python syntax
159
160 Parameters
161 ----------
162
163 code : str
164 IPython code, to be transformed to pure Python
165 """
166 shell = InteractiveShell.instance()
167 return shell.input_transformer_manager.transform_cell(code)
@@ -1,56 +1,56 b''
1 1 {%- extends 'null.tpl' -%}
2 2
3 3 {% block in_prompt %}
4 4 # In[{{cell.prompt_number if cell.prompt_number else ' '}}]:
5 5 {% endblock in_prompt %}
6 6
7 7 {% block output_prompt %}
8 8 # Out[{{cell.prompt_number}}]:{% endblock output_prompt %}
9 9
10 {% block input %}{{ cell.input }}
10 {% block input %}{{ cell.input | ipython2python }}
11 11 {% endblock input %}
12 12
13 13
14 14 {# Those Two are for error displaying
15 15 even if the first one seem to do nothing,
16 16 it introduces a new line
17 17
18 18 #}
19 19 {% block pyerr %}{{ super() }}
20 20 {% endblock pyerr %}
21 21
22 22 {% block traceback_line %}
23 23 {{ line |indent| strip_ansi }}{% endblock traceback_line %}
24 24 {# .... #}
25 25
26 26
27 27 {% block pyout %}
28 28 {{ output.text| indent | comment_lines }}
29 29 {% endblock pyout %}
30 30
31 31 {% block stream %}
32 32 {{ output.text| indent | comment_lines }}
33 33 {% endblock stream %}
34 34
35 35
36 36
37 37
38 38 {% block display_data scoped %}
39 39 # image file:
40 40 {% endblock display_data %}
41 41
42 42 {% block markdowncell scoped %}
43 43 {{ cell.source | comment_lines }}
44 44 {% endblock markdowncell %}
45 45
46 46 {% block headingcell scoped %}
47 47 {{ '#' * cell.level }}{{ cell.source | replace('\n', ' ') | comment_lines }}
48 48 {% endblock headingcell %}
49 49
50 50 {% block rawcell scoped %}
51 51 {{ cell.source | comment_lines }}
52 52 {% endblock rawcell %}
53 53
54 54 {% block unknowncell scoped %}
55 55 unknown type {{cell.type}}
56 56 {% endblock unknowncell %}
General Comments 0
You need to be logged in to leave comments. Login now