1#!/usr/bin/env python 2"""NbConvert is a utility for conversion of .ipynb files. 3 4Command-line interface for the NbConvert conversion utility. 5""" 6 7# Copyright (c) IPython Development Team. 8# Distributed under the terms of the Modified BSD License. 9 10from __future__ import print_function 11 12import logging 13import sys 14import os 15import glob 16import asyncio 17from textwrap import fill, dedent 18from ipython_genutils.text import indent 19 20from jupyter_core.application import JupyterApp, base_aliases, base_flags 21from traitlets.config import catch_config_error, Configurable 22from traitlets import ( 23 Unicode, List, Instance, DottedObjectName, Type, Bool, 24 default, observe, 25) 26 27from traitlets.utils.importstring import import_item 28 29from .exporters.base import get_export_names, get_exporter 30from .exporters.templateexporter import TemplateExporter 31from nbconvert import exporters, preprocessors, writers, postprocessors, __version__ 32from .utils.base import NbConvertBase 33from .utils.exceptions import ConversionException 34from .utils.io import unicode_stdin_stream 35 36#----------------------------------------------------------------------------- 37#Classes and functions 38#----------------------------------------------------------------------------- 39 40class DottedOrNone(DottedObjectName): 41 """A string holding a valid dotted object name in Python, such as A.b3._c 42 Also allows for None type. 43 """ 44 default_value = u'' 45 46 def validate(self, obj, value): 47 if value is not None and len(value) > 0: 48 return super().validate(obj, value) 49 else: 50 return value 51 52nbconvert_aliases = {} 53nbconvert_aliases.update(base_aliases) 54nbconvert_aliases.update({ 55 'to' : 'NbConvertApp.export_format', 56 'template' : 'TemplateExporter.template_name', 57 'template-file' : 'TemplateExporter.template_file', 58 'writer' : 'NbConvertApp.writer_class', 59 'post': 'NbConvertApp.postprocessor_class', 60 'output': 'NbConvertApp.output_base', 61 'output-dir': 'FilesWriter.build_directory', 62 'reveal-prefix': 'SlidesExporter.reveal_url_prefix', 63 'nbformat': 'NotebookExporter.nbformat_version', 64}) 65 66nbconvert_flags = {} 67nbconvert_flags.update(base_flags) 68nbconvert_flags.update({ 69 'execute' : ( 70 {'ExecutePreprocessor' : {'enabled' : True}}, 71 "Execute the notebook prior to export." 72 ), 73 'allow-errors' : ( 74 {'ExecutePreprocessor' : {'allow_errors' : True}}, 75 ("Continue notebook execution even if one of the cells throws " 76 "an error and include the error message in the cell output " 77 "(the default behaviour is to abort conversion). This flag " 78 "is only relevant if '--execute' was specified, too.") 79 ), 80 'stdin' : ( 81 {'NbConvertApp' : { 82 'from_stdin' : True, 83 } 84 }, 85 "read a single notebook file from stdin. Write the resulting notebook with default basename 'notebook.*'" 86 ), 87 'stdout' : ( 88 {'NbConvertApp' : {'writer_class' : "StdoutWriter"}}, 89 "Write notebook output to stdout instead of files." 90 ), 91 'inplace' : ( 92 { 93 'NbConvertApp' : { 94 'use_output_suffix' : False, 95 'export_format' : 'notebook', 96 }, 97 'FilesWriter' : {'build_directory': ''}, 98 }, 99 """Run nbconvert in place, overwriting the existing notebook (only 100 relevant when converting to notebook format)""" 101 ), 102 'clear-output' : ( 103 { 104 'NbConvertApp' : { 105 'use_output_suffix' : False, 106 'export_format' : 'notebook', 107 }, 108 'FilesWriter' : {'build_directory': ''}, 109 'ClearOutputPreprocessor' : {'enabled' : True}, 110 }, 111 """Clear output of current file and save in place, 112 overwriting the existing notebook. """ 113 ), 114 'no-prompt' : ( 115 {'TemplateExporter' : { 116 'exclude_input_prompt' : True, 117 'exclude_output_prompt' : True, 118 } 119 }, 120 "Exclude input and output prompts from converted document." 121 ), 122 'no-input' : ( 123 {'TemplateExporter' : { 124 'exclude_output_prompt' : True, 125 'exclude_input': True, 126 'exclude_input_prompt': True, 127 } 128 }, 129 """Exclude input cells and output prompts from converted document. 130 This mode is ideal for generating code-free reports.""" 131 ), 132 'allow-chromium-download' : ( 133 {'WebPDFExporter' : { 134 'allow_chromium_download' : True, 135 } 136 }, 137 """Whether to allow downloading chromium if no suitable version is found on the system.""" 138 ), 139 'disable-chromium-sandbox': ( 140 {'WebPDFExporter': { 141 'disable_sandbox': True, 142 } 143 }, 144 """Disable chromium security sandbox when converting to PDF..""" 145 ), 146 'show-input' : ( 147 {'TemplateExporter' : { 148 'exclude_input': False, 149 } 150 }, 151 """Shows code input. This is flag is only useful for dejavu users.""" 152 ), 153}) 154 155 156class NbConvertApp(JupyterApp): 157 """Application used to convert from notebook file type (``*.ipynb``)""" 158 159 version = __version__ 160 name = 'jupyter-nbconvert' 161 aliases = nbconvert_aliases 162 flags = nbconvert_flags 163 164 @default('log_level') 165 def _log_level_default(self): 166 return logging.INFO 167 168 classes = List() 169 @default('classes') 170 def _classes_default(self): 171 classes = [NbConvertBase] 172 for pkg in (exporters, preprocessors, writers, postprocessors): 173 for name in dir(pkg): 174 cls = getattr(pkg, name) 175 if isinstance(cls, type) and issubclass(cls, Configurable): 176 classes.append(cls) 177 178 return classes 179 180 description = Unicode( 181 u"""This application is used to convert notebook files (*.ipynb) 182 to various other formats. 183 184 WARNING: THE COMMANDLINE INTERFACE MAY CHANGE IN FUTURE RELEASES.""") 185 186 output_base = Unicode('', help='''overwrite base name use for output files. 187 can only be used when converting one notebook at a time. 188 ''').tag(config=True) 189 190 use_output_suffix = Bool( 191 True, 192 help="""Whether to apply a suffix prior to the extension (only relevant 193 when converting to notebook format). The suffix is determined by 194 the exporter, and is usually '.nbconvert'.""" 195 ).tag(config=True) 196 197 output_files_dir = Unicode('{notebook_name}_files', 198 help='''Directory to copy extra files (figures) to. 199 '{notebook_name}' in the string will be converted to notebook 200 basename.''' 201 ).tag(config=True) 202 203 examples = Unicode(u""" 204 The simplest way to use nbconvert is 205 206 > jupyter nbconvert mynotebook.ipynb --to html 207 208 Options include {formats}. 209 210 > jupyter nbconvert --to latex mynotebook.ipynb 211 212 Both HTML and LaTeX support multiple output templates. LaTeX includes 213 'base', 'article' and 'report'. HTML includes 'basic' and 'full'. You 214 can specify the flavor of the format used. 215 216 > jupyter nbconvert --to html --template lab mynotebook.ipynb 217 218 You can also pipe the output to stdout, rather than a file 219 220 > jupyter nbconvert mynotebook.ipynb --stdout 221 222 PDF is generated via latex 223 224 > jupyter nbconvert mynotebook.ipynb --to pdf 225 226 You can get (and serve) a Reveal.js-powered slideshow 227 228 > jupyter nbconvert myslides.ipynb --to slides --post serve 229 230 Multiple notebooks can be given at the command line in a couple of 231 different ways: 232 233 > jupyter nbconvert notebook*.ipynb 234 > jupyter nbconvert notebook1.ipynb notebook2.ipynb 235 236 or you can specify the notebooks list in a config file, containing:: 237 238 c.NbConvertApp.notebooks = ["my_notebook.ipynb"] 239 240 > jupyter nbconvert --config mycfg.py 241 """.format(formats=get_export_names())) 242 243 # Writer specific variables 244 writer = Instance('nbconvert.writers.base.WriterBase', 245 help="""Instance of the writer class used to write the 246 results of the conversion.""", allow_none=True) 247 writer_class = DottedObjectName('FilesWriter', 248 help="""Writer class used to write the 249 results of the conversion""").tag(config=True) 250 writer_aliases = {'fileswriter': 'nbconvert.writers.files.FilesWriter', 251 'debugwriter': 'nbconvert.writers.debug.DebugWriter', 252 'stdoutwriter': 'nbconvert.writers.stdout.StdoutWriter'} 253 writer_factory = Type(allow_none=True) 254 255 @observe('writer_class') 256 def _writer_class_changed(self, change): 257 new = change['new'] 258 if new.lower() in self.writer_aliases: 259 new = self.writer_aliases[new.lower()] 260 self.writer_factory = import_item(new) 261 262 # Post-processor specific variables 263 postprocessor = Instance('nbconvert.postprocessors.base.PostProcessorBase', 264 help="""Instance of the PostProcessor class used to write the 265 results of the conversion.""", allow_none=True) 266 267 postprocessor_class = DottedOrNone( 268 help="""PostProcessor class used to write the 269 results of the conversion""" 270 ).tag(config=True) 271 postprocessor_aliases = {'serve': 'nbconvert.postprocessors.serve.ServePostProcessor'} 272 postprocessor_factory = Type(None, allow_none=True) 273 274 @observe('postprocessor_class') 275 def _postprocessor_class_changed(self, change): 276 new = change['new'] 277 if new.lower() in self.postprocessor_aliases: 278 new = self.postprocessor_aliases[new.lower()] 279 if new: 280 self.postprocessor_factory = import_item(new) 281 282 export_format = Unicode( 283 allow_none=False, 284 help="""The export format to be used, either one of the built-in formats 285 {formats} 286 or a dotted object name that represents the import path for an 287 ``Exporter`` class""".format(formats=get_export_names()) 288 ).tag(config=True) 289 290 notebooks = List([], help="""List of notebooks to convert. 291 Wildcards are supported. 292 Filenames passed positionally will be added to the list. 293 """ 294 ).tag(config=True) 295 from_stdin = Bool(False, help="read a single notebook from stdin.").tag(config=True) 296 297 @catch_config_error 298 def initialize(self, argv=None): 299 """Initialize application, notebooks, writer, and postprocessor""" 300 # See https://bugs.python.org/issue37373 :( 301 if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'): 302 asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 303 304 self.init_syspath() 305 super().initialize(argv) 306 self.init_notebooks() 307 self.init_writer() 308 self.init_postprocessor() 309 310 def init_syspath(self): 311 """Add the cwd to the sys.path ($PYTHONPATH)""" 312 sys.path.insert(0, os.getcwd()) 313 314 def init_notebooks(self): 315 """Construct the list of notebooks. 316 317 If notebooks are passed on the command-line, 318 they override (rather than add) notebooks specified in config files. 319 Glob each notebook to replace notebook patterns with filenames. 320 """ 321 322 # Specifying notebooks on the command-line overrides (rather than 323 # adds) the notebook list 324 if self.extra_args: 325 patterns = self.extra_args 326 else: 327 patterns = self.notebooks 328 329 # Use glob to replace all the notebook patterns with filenames. 330 filenames = [] 331 for pattern in patterns: 332 333 # Use glob to find matching filenames. Allow the user to convert 334 # notebooks without having to type the extension. 335 globbed_files = glob.glob(pattern) 336 globbed_files.extend(glob.glob(pattern + '.ipynb')) 337 if not globbed_files: 338 self.log.warning("pattern %r matched no files", pattern) 339 340 for filename in globbed_files: 341 if not filename in filenames: 342 filenames.append(filename) 343 self.notebooks = filenames 344 345 def init_writer(self): 346 """Initialize the writer (which is stateless)""" 347 self._writer_class_changed({ 'new': self.writer_class }) 348 self.writer = self.writer_factory(parent=self) 349 if hasattr(self.writer, 'build_directory') and self.writer.build_directory != '': 350 self.use_output_suffix = False 351 352 def init_postprocessor(self): 353 """Initialize the postprocessor (which is stateless)""" 354 self._postprocessor_class_changed({'new': self.postprocessor_class}) 355 if self.postprocessor_factory: 356 self.postprocessor = self.postprocessor_factory(parent=self) 357 358 def start(self): 359 """Run start after initialization process has completed""" 360 super().start() 361 self.convert_notebooks() 362 363 def init_single_notebook_resources(self, notebook_filename): 364 """Step 1: Initialize resources 365 366 This initializes the resources dictionary for a single notebook. 367 368 Returns 369 ------- 370 dict 371 resources dictionary for a single notebook that MUST include the following keys: 372 - config_dir: the location of the Jupyter config directory 373 - unique_key: the notebook name 374 - output_files_dir: a directory where output files (not 375 including the notebook itself) should be saved 376 """ 377 basename = os.path.basename(notebook_filename) 378 notebook_name = basename[:basename.rfind('.')] 379 if self.output_base: 380 # strip duplicate extension from output_base, to avoid Basename.ext.ext 381 if getattr(self.exporter, 'file_extension', False): 382 base, ext = os.path.splitext(self.output_base) 383 if ext == self.exporter.file_extension: 384 self.output_base = base 385 notebook_name = self.output_base 386 387 self.log.debug("Notebook name is '%s'", notebook_name) 388 389 # first initialize the resources we want to use 390 resources = {} 391 resources['config_dir'] = self.config_dir 392 resources['unique_key'] = notebook_name 393 394 output_files_dir = (self.output_files_dir 395 .format(notebook_name=notebook_name)) 396 397 resources['output_files_dir'] = output_files_dir 398 399 return resources 400 401 def export_single_notebook(self, notebook_filename, resources, input_buffer=None): 402 """Step 2: Export the notebook 403 404 Exports the notebook to a particular format according to the specified 405 exporter. This function returns the output and (possibly modified) 406 resources from the exporter. 407 408 Parameters 409 ---------- 410 notebook_filename : str 411 name of notebook file. 412 resources : dict 413 input_buffer : 414 readable file-like object returning unicode. 415 if not None, notebook_filename is ignored 416 417 Returns 418 ------- 419 output 420 dict 421 resources (possibly modified) 422 """ 423 try: 424 if input_buffer is not None: 425 output, resources = self.exporter.from_file(input_buffer, resources=resources) 426 else: 427 output, resources = self.exporter.from_filename(notebook_filename, resources=resources) 428 except ConversionException: 429 self.log.error("Error while converting '%s'", notebook_filename, exc_info=True) 430 self.exit(1) 431 432 return output, resources 433 434 def write_single_notebook(self, output, resources): 435 """Step 3: Write the notebook to file 436 437 This writes output from the exporter to file using the specified writer. 438 It returns the results from the writer. 439 440 Parameters 441 ---------- 442 output : 443 resources : dict 444 resources for a single notebook including name, config directory 445 and directory to save output 446 447 Returns 448 ------- 449 file 450 results from the specified writer output of exporter 451 """ 452 if 'unique_key' not in resources: 453 raise KeyError("unique_key MUST be specified in the resources, but it is not") 454 455 notebook_name = resources['unique_key'] 456 if self.use_output_suffix and not self.output_base: 457 notebook_name += resources.get('output_suffix', '') 458 459 write_results = self.writer.write( 460 output, resources, notebook_name=notebook_name) 461 return write_results 462 463 def postprocess_single_notebook(self, write_results): 464 """Step 4: Post-process the written file 465 466 Only used if a postprocessor has been specified. After the 467 converted notebook is written to a file in Step 3, this post-processes 468 the notebook. 469 """ 470 # Post-process if post processor has been defined. 471 if hasattr(self, 'postprocessor') and self.postprocessor: 472 self.postprocessor(write_results) 473 474 def convert_single_notebook(self, notebook_filename, input_buffer=None): 475 """Convert a single notebook. 476 477 Performs the following steps: 478 479 1. Initialize notebook resources 480 2. Export the notebook to a particular format 481 3. Write the exported notebook to file 482 4. (Maybe) postprocess the written file 483 484 Parameters 485 ---------- 486 notebook_filename : str 487 input_buffer : 488 If input_buffer is not None, conversion is done and the buffer is 489 used as source into a file basenamed by the notebook_filename 490 argument. 491 """ 492 if input_buffer is None: 493 self.log.info("Converting notebook %s to %s", notebook_filename, self.export_format) 494 else: 495 self.log.info("Converting notebook into %s", self.export_format) 496 497 resources = self.init_single_notebook_resources(notebook_filename) 498 output, resources = self.export_single_notebook(notebook_filename, resources, input_buffer=input_buffer) 499 write_results = self.write_single_notebook(output, resources) 500 self.postprocess_single_notebook(write_results) 501 502 def convert_notebooks(self): 503 """Convert the notebooks in the self.notebook traitlet """ 504 # check that the output base isn't specified if there is more than 505 # one notebook to convert 506 if self.output_base != '' and len(self.notebooks) > 1: 507 self.log.error( 508 """ 509 UsageError: --output flag or `NbConvertApp.output_base` config option 510 cannot be used when converting multiple notebooks. 511 """ 512 ) 513 self.exit(1) 514 515 # no notebooks to convert! 516 if len(self.notebooks) == 0 and not self.from_stdin: 517 self.print_help() 518 sys.exit(-1) 519 520 if not self.export_format: 521 raise ValueError( 522 "Please specify an output format with '--to <format>'." 523 f"\nThe following formats are available: {get_export_names()}" 524 ) 525 526 # initialize the exporter 527 cls = get_exporter(self.export_format) 528 self.exporter = cls(config=self.config) 529 530 # convert each notebook 531 if not self.from_stdin: 532 for notebook_filename in self.notebooks: 533 self.convert_single_notebook(notebook_filename) 534 else: 535 input_buffer = unicode_stdin_stream() 536 # default name when conversion from stdin 537 self.convert_single_notebook("notebook.ipynb", input_buffer=input_buffer) 538 539 def document_flag_help(self): 540 """ 541 Return a string containing descriptions of all the flags. 542 """ 543 flags = "The following flags are defined:\n\n" 544 for flag, (cfg, fhelp) in self.flags.items(): 545 flags += "{}\n".format(flag) 546 flags += indent(fill(fhelp, 80)) + '\n\n' 547 flags += indent(fill("Long Form: "+str(cfg), 80)) + '\n\n' 548 return flags 549 550 def document_alias_help(self): 551 """Return a string containing all of the aliases""" 552 553 aliases = "The folowing aliases are defined:\n\n" 554 for alias, longname in self.aliases.items(): 555 aliases += "\t**{}** ({})\n\n".format(alias, longname) 556 return aliases 557 558 def document_config_options(self): 559 """ 560 Provides a much improves version of the configuration documentation by 561 breaking the configuration options into app, exporter, writer, 562 preprocessor, postprocessor, and other sections. 563 """ 564 categories = {category: [c for c in self._classes_inc_parents() if category in c.__name__.lower()] 565 for category in ['app', 'exporter', 'writer', 'preprocessor', 'postprocessor']} 566 accounted_for = {c for category in categories.values() for c in category} 567 categories['other']= [c for c in self._classes_inc_parents() if c not in accounted_for] 568 569 header = dedent(""" 570 {section} Options 571 ----------------------- 572 573 """) 574 sections = "" 575 for category in categories: 576 sections += header.format(section=category.title()) 577 if category in ['exporter','preprocessor','writer']: 578 sections += ".. image:: _static/{image}_inheritance.png\n\n".format(image=category) 579 sections += '\n'.join(c.class_config_rst_doc() for c in categories[category]) 580 581 return sections.replace(' : ',r' \: ') 582 583 584class DejavuApp(NbConvertApp): 585 def initialize(self, argv=None): 586 self.config.TemplateExporter.exclude_input = True 587 self.config.TemplateExporter.exclude_output_prompt = True 588 self.config.TemplateExporter.exclude_input_prompt = True 589 self.config.ExecutePreprocessor.enabled = True 590 self.config.WebPDFExporter.paginate = False 591 592 super().initialize(argv) 593 594 @default('export_format') 595 def default_export_format(self): 596 return 'html' 597 598#----------------------------------------------------------------------------- 599# Main entry point 600#----------------------------------------------------------------------------- 601 602main = launch_new_instance = NbConvertApp.launch_instance 603dejavu_main = DejavuApp.launch_instance 604