##// END OF EJS Templates
Break up convert_notebooks so that it is easier to subclass
Jessica B. Hamrick -
Show More
@@ -1,340 +1,407
1 1 #!/usr/bin/env python
2 2 """NbConvert is a utility for conversion of .ipynb files.
3 3
4 4 Command-line interface for the NbConvert conversion utility.
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 from __future__ import print_function
11 11
12 12 import logging
13 13 import sys
14 14 import os
15 15 import glob
16 16
17 17 from IPython.core.application import BaseIPythonApplication, base_aliases, base_flags
18 18 from IPython.core.profiledir import ProfileDir
19 19 from IPython.config import catch_config_error, Configurable
20 20 from IPython.utils.traitlets import (
21 21 Unicode, List, Instance, DottedObjectName, Type, CaselessStrEnum, Bool,
22 22 )
23 23 from IPython.utils.importstring import import_item
24 24
25 25 from .exporters.export import get_export_names, exporter_map
26 26 from IPython.nbconvert import exporters, preprocessors, writers, postprocessors
27 27 from .utils.base import NbConvertBase
28 28 from .utils.exceptions import ConversionException
29 29
30 30 #-----------------------------------------------------------------------------
31 31 #Classes and functions
32 32 #-----------------------------------------------------------------------------
33 33
34 34 class DottedOrNone(DottedObjectName):
35 35 """
36 36 A string holding a valid dotted object name in Python, such as A.b3._c
37 37 Also allows for None type."""
38 38
39 39 default_value = u''
40 40
41 41 def validate(self, obj, value):
42 42 if value is not None and len(value) > 0:
43 43 return super(DottedOrNone, self).validate(obj, value)
44 44 else:
45 45 return value
46 46
47 47 nbconvert_aliases = {}
48 48 nbconvert_aliases.update(base_aliases)
49 49 nbconvert_aliases.update({
50 50 'to' : 'NbConvertApp.export_format',
51 51 'template' : 'TemplateExporter.template_file',
52 52 'writer' : 'NbConvertApp.writer_class',
53 53 'post': 'NbConvertApp.postprocessor_class',
54 54 'output': 'NbConvertApp.output_base',
55 55 'reveal-prefix': 'RevealHelpPreprocessor.url_prefix',
56 56 'nbformat': 'NotebookExporter.nbformat_version',
57 57 })
58 58
59 59 nbconvert_flags = {}
60 60 nbconvert_flags.update(base_flags)
61 61 nbconvert_flags.update({
62 62 'execute' : (
63 63 {'ExecutePreprocessor' : {'enabled' : True}},
64 64 "Execute the notebook prior to export."
65 65 ),
66 66 'stdout' : (
67 67 {'NbConvertApp' : {'writer_class' : "StdoutWriter"}},
68 68 "Write notebook output to stdout instead of files."
69 69 ),
70 70 'inplace' : (
71 71 {
72 72 'NbConvertApp' : {'use_output_suffix' : False},
73 73 'FilesWriter': {'build_directory': ''}
74 74 },
75 75 """Run nbconvert in place, overwriting the existing notebook (only
76 76 relevant when converting to notebook format)"""
77 77 )
78 78 })
79 79
80 80
81 81 class NbConvertApp(BaseIPythonApplication):
82 82 """Application used to convert from notebook file type (``*.ipynb``)"""
83 83
84 84 name = 'ipython-nbconvert'
85 85 aliases = nbconvert_aliases
86 86 flags = nbconvert_flags
87 87
88 88 def _log_level_default(self):
89 89 return logging.INFO
90 90
91 91 def _classes_default(self):
92 92 classes = [NbConvertBase, ProfileDir]
93 93 for pkg in (exporters, preprocessors, writers, postprocessors):
94 94 for name in dir(pkg):
95 95 cls = getattr(pkg, name)
96 96 if isinstance(cls, type) and issubclass(cls, Configurable):
97 97 classes.append(cls)
98 98
99 99 return classes
100 100
101 101 description = Unicode(
102 102 u"""This application is used to convert notebook files (*.ipynb)
103 103 to various other formats.
104 104
105 105 WARNING: THE COMMANDLINE INTERFACE MAY CHANGE IN FUTURE RELEASES.""")
106 106
107 107 output_base = Unicode('', config=True, help='''overwrite base name use for output files.
108 108 can only be used when converting one notebook at a time.
109 109 ''')
110 110
111 111 use_output_suffix = Bool(
112 112 True,
113 113 config=True,
114 114 help="""Whether to apply a suffix prior to the extension (only relevant
115 115 when converting to notebook format). The suffix is determined by
116 116 the exporter, and is usually '.nbconvert'.""")
117 117
118 118 examples = Unicode(u"""
119 119 The simplest way to use nbconvert is
120 120
121 121 > ipython nbconvert mynotebook.ipynb
122 122
123 123 which will convert mynotebook.ipynb to the default format (probably HTML).
124 124
125 125 You can specify the export format with `--to`.
126 126 Options include {0}
127 127
128 128 > ipython nbconvert --to latex mynotebook.ipynb
129 129
130 130 Both HTML and LaTeX support multiple output templates. LaTeX includes
131 131 'base', 'article' and 'report'. HTML includes 'basic' and 'full'. You
132 132 can specify the flavor of the format used.
133 133
134 134 > ipython nbconvert --to html --template basic mynotebook.ipynb
135 135
136 136 You can also pipe the output to stdout, rather than a file
137 137
138 138 > ipython nbconvert mynotebook.ipynb --stdout
139 139
140 140 PDF is generated via latex
141 141
142 142 > ipython nbconvert mynotebook.ipynb --to pdf
143 143
144 144 You can get (and serve) a Reveal.js-powered slideshow
145 145
146 146 > ipython nbconvert myslides.ipynb --to slides --post serve
147 147
148 148 Multiple notebooks can be given at the command line in a couple of
149 149 different ways:
150 150
151 151 > ipython nbconvert notebook*.ipynb
152 152 > ipython nbconvert notebook1.ipynb notebook2.ipynb
153 153
154 154 or you can specify the notebooks list in a config file, containing::
155 155
156 156 c.NbConvertApp.notebooks = ["my_notebook.ipynb"]
157 157
158 158 > ipython nbconvert --config mycfg.py
159 159 """.format(get_export_names()))
160 160
161 161 # Writer specific variables
162 162 writer = Instance('IPython.nbconvert.writers.base.WriterBase',
163 163 help="""Instance of the writer class used to write the
164 164 results of the conversion.""")
165 165 writer_class = DottedObjectName('FilesWriter', config=True,
166 166 help="""Writer class used to write the
167 167 results of the conversion""")
168 168 writer_aliases = {'fileswriter': 'IPython.nbconvert.writers.files.FilesWriter',
169 169 'debugwriter': 'IPython.nbconvert.writers.debug.DebugWriter',
170 170 'stdoutwriter': 'IPython.nbconvert.writers.stdout.StdoutWriter'}
171 171 writer_factory = Type()
172 172
173 173 def _writer_class_changed(self, name, old, new):
174 174 if new.lower() in self.writer_aliases:
175 175 new = self.writer_aliases[new.lower()]
176 176 self.writer_factory = import_item(new)
177 177
178 178 # Post-processor specific variables
179 179 postprocessor = Instance('IPython.nbconvert.postprocessors.base.PostProcessorBase',
180 180 help="""Instance of the PostProcessor class used to write the
181 181 results of the conversion.""")
182 182
183 183 postprocessor_class = DottedOrNone(config=True,
184 184 help="""PostProcessor class used to write the
185 185 results of the conversion""")
186 186 postprocessor_aliases = {'serve': 'IPython.nbconvert.postprocessors.serve.ServePostProcessor'}
187 187 postprocessor_factory = Type()
188 188
189 189 def _postprocessor_class_changed(self, name, old, new):
190 190 if new.lower() in self.postprocessor_aliases:
191 191 new = self.postprocessor_aliases[new.lower()]
192 192 if new:
193 193 self.postprocessor_factory = import_item(new)
194 194
195 195
196 196 # Other configurable variables
197 197 export_format = CaselessStrEnum(get_export_names(),
198 198 default_value="html",
199 199 config=True,
200 200 help="""The export format to be used."""
201 201 )
202 202
203 203 notebooks = List([], config=True, help="""List of notebooks to convert.
204 204 Wildcards are supported.
205 205 Filenames passed positionally will be added to the list.
206 206 """)
207 207
208 208 @catch_config_error
209 209 def initialize(self, argv=None):
210 210 self.init_syspath()
211 211 super(NbConvertApp, self).initialize(argv)
212 212 self.init_notebooks()
213 213 self.init_writer()
214 214 self.init_postprocessor()
215 215
216 216
217 217
218 218 def init_syspath(self):
219 219 """
220 220 Add the cwd to the sys.path ($PYTHONPATH)
221 221 """
222 222 sys.path.insert(0, os.getcwd())
223 223
224 224
225 225 def init_notebooks(self):
226 226 """Construct the list of notebooks.
227 227 If notebooks are passed on the command-line,
228 228 they override notebooks specified in config files.
229 229 Glob each notebook to replace notebook patterns with filenames.
230 230 """
231 231
232 232 # Specifying notebooks on the command-line overrides (rather than adds)
233 233 # the notebook list
234 234 if self.extra_args:
235 235 patterns = self.extra_args
236 236 else:
237 237 patterns = self.notebooks
238 238
239 239 # Use glob to replace all the notebook patterns with filenames.
240 240 filenames = []
241 241 for pattern in patterns:
242 242
243 243 # Use glob to find matching filenames. Allow the user to convert
244 244 # notebooks without having to type the extension.
245 245 globbed_files = glob.glob(pattern)
246 246 globbed_files.extend(glob.glob(pattern + '.ipynb'))
247 247 if not globbed_files:
248 248 self.log.warn("pattern %r matched no files", pattern)
249 249
250 250 for filename in globbed_files:
251 251 if not filename in filenames:
252 252 filenames.append(filename)
253 253 self.notebooks = filenames
254 254
255 255 def init_writer(self):
256 256 """
257 257 Initialize the writer (which is stateless)
258 258 """
259 259 self._writer_class_changed(None, self.writer_class, self.writer_class)
260 260 self.writer = self.writer_factory(parent=self)
261 261 if hasattr(self.writer, 'build_directory') and self.writer.build_directory != '':
262 262 self.use_output_suffix = False
263 263
264 264 def init_postprocessor(self):
265 265 """
266 266 Initialize the postprocessor (which is stateless)
267 267 """
268 268 self._postprocessor_class_changed(None, self.postprocessor_class,
269 269 self.postprocessor_class)
270 270 if self.postprocessor_factory:
271 271 self.postprocessor = self.postprocessor_factory(parent=self)
272 272
273 273 def start(self):
274 274 """
275 275 Ran after initialization completed
276 276 """
277 277 super(NbConvertApp, self).start()
278 278 self.convert_notebooks()
279 279
280 def init_single_notebook_resources(self, notebook_filename):
281 """Step 1: Initialize resources
282
283 This intializes the resources dictionary for a single notebook. This
284 method should return the resources dictionary, and MUST include the
285 following keys:
286
287 - profile_dir: the location of the profile directory
288 - unique_key: the notebook name
289 - output_files_dir: a directory where output files (not including
290 the notebook itself) should be saved
291
292 """
293
294 # Get a unique key for the notebook and set it in the resources object.
295 basename = os.path.basename(notebook_filename)
296 notebook_name = basename[:basename.rfind('.')]
297 if self.output_base:
298 # strip duplicate extension from output_base, to avoid Basname.ext.ext
299 if getattr(self.exporter, 'file_extension', False):
300 base, ext = os.path.splitext(self.output_base)
301 if ext == self.exporter.file_extension:
302 self.output_base = base
303 notebook_name = self.output_base
304
305 self.log.debug("Notebook name is '%s'", notebook_name)
306
307 # first initialize the resources we want to use
308 resources = {}
309 resources['profile_dir'] = self.profile_dir.location
310 resources['unique_key'] = notebook_name
311 resources['output_files_dir'] = '%s_files' % notebook_name
312
313 return resources
314
315 def export_single_notebook(self, notebook_filename, resources):
316 """Step 2: Export the notebook
317
318 Exports the notebook to a particular format according to the specified
319 exporter. This function returns the output and (possibly modified)
320 resources from the exporter.
321
322 """
323 try:
324 output, resources = self.exporter.from_filename(notebook_filename, resources=resources)
325 except ConversionException:
326 self.log.error("Error while converting '%s'", notebook_filename, exc_info=True)
327 self.exit(1)
328
329 return output, resources
330
331 def write_single_notebook(self, output, resources):
332 """Step 3: Write the notebook to file
333
334 This writes output from the exporter to file using the specified writer.
335 It returns the results from the writer.
336
337 """
338 if 'unique_key' not in resources:
339 raise KeyError("unique_key MUST be specified in the resources, but it is not")
340
341 notebook_name = resources['unique_key']
342 if self.use_output_suffix and not self.output_base:
343 notebook_name += resources.get('output_suffix', '')
344
345 write_results = self.writer.write(
346 output, resources, notebook_name=notebook_name)
347 return write_results
348
349 def postprocess_single_notebook(self, write_results):
350 """Step 4: Postprocess the notebook
351
352 This postprocesses the notebook after it has been written, taking as an
353 argument the results of writing the notebook to file. This only actually
354 does anything if a postprocessor has actually been specified.
355
356 """
357 # Post-process if post processor has been defined.
358 if hasattr(self, 'postprocessor') and self.postprocessor:
359 self.postprocessor(write_results)
360
361 def convert_single_notebook(self, notebook_filename):
362 """Convert a single notebook. Performs the following steps:
363
364 1. Initialize notebook resources
365 2. Export the notebook to a particular format
366 3. Write the exported notebook to file
367 4. (Maybe) postprocess the written file
368
369 """
370 self.log.info("Converting notebook %s to %s", notebook_filename, self.export_format)
371 resources = self.init_single_notebook_resources(notebook_filename)
372 output, resources = self.export_single_notebook(notebook_filename, resources)
373 write_results = self.write_single_notebook(output, resources)
374 self.postprocess_single_notebook(write_results)
375
280 376 def convert_notebooks(self):
281 377 """
282 378 Convert the notebooks in the self.notebook traitlet
283 379 """
284 # Export each notebook
285 conversion_success = 0
286
380 # check that the output base isn't specified if there is more than
381 # one notebook to convert
287 382 if self.output_base != '' and len(self.notebooks) > 1:
288 383 self.log.error(
289 """UsageError: --output flag or `NbConvertApp.output_base` config option
290 cannot be used when converting multiple notebooks.
291 """)
384 """
385 UsageError: --output flag or `NbConvertApp.output_base` config option
386 cannot be used when converting multiple notebooks.
387 """
388 )
292 389 self.exit(1)
293 390
294 exporter = exporter_map[self.export_format](config=self.config)
391 # initialize the exporter
392 self.exporter = exporter_map[self.export_format](config=self.config)
295 393
296 for notebook_filename in self.notebooks:
297 self.log.info("Converting notebook %s to %s", notebook_filename, self.export_format)
298
299 # Get a unique key for the notebook and set it in the resources object.
300 basename = os.path.basename(notebook_filename)
301 notebook_name = basename[:basename.rfind('.')]
302 if self.output_base:
303 # strip duplicate extension from output_base, to avoid Basname.ext.ext
304 if getattr(exporter, 'file_extension', False):
305 base, ext = os.path.splitext(self.output_base)
306 if ext == exporter.file_extension:
307 self.output_base = base
308 notebook_name = self.output_base
309 resources = {}
310 resources['profile_dir'] = self.profile_dir.location
311 resources['unique_key'] = notebook_name
312 resources['output_files_dir'] = '%s_files' % notebook_name
313
314 # Try to export
315 try:
316 output, resources = exporter.from_filename(notebook_filename, resources=resources)
317 except ConversionException as e:
318 self.log.error("Error while converting '%s'", notebook_filename,
319 exc_info=True)
320 self.exit(1)
321 else:
322 if self.use_output_suffix and 'output_suffix' in resources and not self.output_base:
323 notebook_name += resources['output_suffix']
324 write_results = self.writer.write(output, resources, notebook_name=notebook_name)
325
326 #Post-process if post processor has been defined.
327 if hasattr(self, 'postprocessor') and self.postprocessor:
328 self.postprocessor(write_results)
329 conversion_success += 1
330
331 # If nothing was converted successfully, help the user.
332 if conversion_success == 0:
394 # no notebooks to convert!
395 if len(self.notebooks) == 0:
333 396 self.print_help()
334 397 sys.exit(-1)
398
399 # convert each notebook
400 for notebook_filename in self.notebooks:
401 self.convert_single_notebook(notebook_filename)
335 402
336 403 #-----------------------------------------------------------------------------
337 404 # Main entry point
338 405 #-----------------------------------------------------------------------------
339 406
340 407 launch_new_instance = NbConvertApp.launch_instance
General Comments 0
You need to be logged in to leave comments. Login now