"""Module containing a preprocessor that removes the outputs from code cells""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import os from textwrap import dedent try: from queue import Empty # Py 3 except ImportError: from Queue import Empty # Py 2 from traitlets import List, Unicode, Bool from nbformat.v4 import output_from_msg from .base import Preprocessor from ..utils.exceptions import ConversionException from traitlets import Integer class CellExecutionError(ConversionException): """ Custom exception to propagate exceptions that are raised during notebook execution to the caller. This is mostly useful when using nbconvert as a library, since it allows to deal with failures gracefully. """ def __init__(self, traceback): self.traceback = traceback def __str__(self): return self.traceback class ExecutePreprocessor(Preprocessor): """ Executes all the cells in a notebook """ timeout = Integer(30, config=True, allow_none=True, help=dedent( """ The time to wait (in seconds) for output from executions. If a cell execution takes longer, an exception (TimeoutError on python 3+, RuntimeError on python 2) is raised. `None` or `-1` will disable the timeout. """ ) ) interrupt_on_timeout = Bool( False, config=True, help=dedent( """ If execution of a cell times out, interrupt the kernel and continue executing other cells rather than throwing an error and stopping. """ ) ) allow_errors = Bool( False, config=True, help=dedent( """ If `False` (default), when a cell raises an error the execution is stoppped and a `CellExecutionError` is raised. If `True`, execution errors are ignored and the execution is continued until the end of the notebook. Output from exceptions is included in the cell output in both cases. """ ) ) extra_arguments = List(Unicode()) kernel_name = Unicode( '', config=True, help=dedent( """ Name of kernel to use to execute the cells. If not set, use the kernel_spec embedded in the notebook. """ ) ) raise_on_iopub_timeout = Bool( False, config=True, help=dedent( """ If `False` (default), then the kernel will continue waiting for iopub messages until it receives a kernel idle message, or until a timeout occurs, at which point the currently executing cell will be skipped. If `True`, then an error will be raised after the first timeout. This option generally does not need to be used, but may be useful in contexts where there is the possibility of executing notebooks with memory-consuming infinite loops. """ ) ) def preprocess(self, nb, resources): """ Preprocess notebook executing each code cell. The input argument `nb` is modified in-place. Parameters ---------- nb : NotebookNode Notebook being executed. resources : dictionary Additional resources used in the conversion process. For example, passing ``{'metadata': {'path': run_path}}`` sets the execution path to ``run_path``. Returns ------- nb : NotebookNode The executed notebook. resources : dictionary Additional resources used in the conversion process. """ path = resources.get('metadata', {}).get('path', '') if path == '': path = None from jupyter_client.manager import start_new_kernel kernel_name = nb.metadata.get('kernelspec', {}).get('name', 'python') if self.kernel_name: kernel_name = self.kernel_name self.log.info("Executing notebook with kernel: %s" % kernel_name) self.km, self.kc = start_new_kernel( kernel_name=kernel_name, extra_arguments=self.extra_arguments, stderr=open(os.devnull, 'w'), cwd=path) self.kc.allow_stdin = False try: nb, resources = super(ExecutePreprocessor, self).preprocess(nb, resources) finally: self.kc.stop_channels() self.km.shutdown_kernel(now=True) return nb, resources def preprocess_cell(self, cell, resources, cell_index): """ Executes a single code cell. See base.py for details. To execute all cells see :meth:`preprocess`. """ if cell.cell_type != 'code': return cell, resources outputs = self.run_cell(cell) cell.outputs = outputs if not self.allow_errors: for out in outputs: if out.output_type == 'error': pattern = """\ An error occurred while executing the following cell: ------------------ {cell.source} ------------------ {out.ename}: {out.evalue} """ msg = dedent(pattern).format(out=out, cell=cell) raise CellExecutionError(msg) return cell, resources def run_cell(self, cell): msg_id = self.kc.execute(cell.source) self.log.debug("Executing cell:\n%s", cell.source) # wait for finish, with timeout while True: try: timeout = self.timeout if timeout < 0: timeout = None msg = self.kc.shell_channel.get_msg(timeout=timeout) except Empty: self.log.error("""Timeout waiting for execute reply (%is). If your cell should take longer than this, you can increase the timeout with: c.ExecutePreprocessor.timeout = SECONDS in jupyter_nbconvert_config.py """ % self.timeout) if self.interrupt_on_timeout: self.log.error("Interrupting kernel") self.km.interrupt_kernel() break else: try: exception = TimeoutError except NameError: exception = RuntimeError raise exception("Cell execution timed out, see log" " for details.") if msg['parent_header'].get('msg_id') == msg_id: break else: # not our reply continue outs = [] while True: try: # We've already waited for execute_reply, so all output # should already be waiting. However, on slow networks, like # in certain CI systems, waiting < 1 second might miss messages. # So long as the kernel sends a status:idle message when it # finishes, we won't actually have to wait this long, anyway. msg = self.kc.iopub_channel.get_msg(timeout=4) except Empty: self.log.warn("Timeout waiting for IOPub output") if self.raise_on_iopub_timeout: raise RuntimeError("Timeout waiting for IOPub output") else: break if msg['parent_header'].get('msg_id') != msg_id: # not an output from our execution continue msg_type = msg['msg_type'] self.log.debug("output: %s", msg_type) content = msg['content'] # set the prompt number for the input and the output if 'execution_count' in content: cell['execution_count'] = content['execution_count'] if msg_type == 'status': if content['execution_state'] == 'idle': break else: continue elif msg_type == 'execute_input': continue elif msg_type == 'clear_output': outs = [] continue elif msg_type.startswith('comm'): continue try: out = output_from_msg(msg) except ValueError: self.log.error("unhandled iopub msg: " + msg_type) else: outs.append(out) return outs