Repository: ipython/ipython

Issue:
add file_to_run=file.ipynb capability to the notebook
over at ipython/nbconvert#13, @Krastanov requested the ability to re-run all cells in a notebook before doing a conversion. 
@Krastanov said:

> Is there a way to evaluate all cells of a notebook before we use nbconvert?
> 
> Why:
> I am writing a script that will convert our example notebooks to some static form to be part of the documentation. But many of > the examples contain plots, which take too much space to be kept in git so we are keeping only the unevaluated notebooks in the tree. However we want to generate documentation containing these images, so we must generate them somehow?

I suggested this can legitimately live in the `ipython notebook` entry point (hence this enhancement request), but can also see the advantages of having that functionality in the (forthcoming) `ipython nbconvert`

This would be the equivalent of  clicking on "Run All" under the "Cell" menu of a notebook and saving it, without needing a web browser.

This is somewhat similar to, but different from #945.



Discussion:
The question is what are the desired semantics: do we want to run the cells and update the nb contents in-place?  That's currently more or less impossible, since the code responsible for much of that is actually _in the browser_.  Now, being able to re-execute the code in a notebook by itself, that's a different story.  But that would be only to re-generate figures on-disk, for example...

Given how much of the document logic is in JavaScript, I think this is a no-go for the foreseeable future, and I'm not sure it's worth keeping it open since it's likely unrealistic to change even in the long run.  @ellisonbg, @minrk, thoughts on this?  I'm trying to be fairly aggressive in  issue triage so we keep open things we can realistically work on, and I'm not quite sure this falls in that category...


Resolving patch:
diff --git a/IPython/core/completerlib.py b/IPython/core/completerlib.py
--- a/IPython/core/completerlib.py
+++ b/IPython/core/completerlib.py
@@ -54,7 +54,7 @@
                        r'|'.join(re.escape(s[0]) for s in imp.get_suffixes()))
 
 # RE for the ipython %run command (python + ipython scripts)
-magic_run_re = re.compile(r'.*(\.ipy|\.py[w]?)$')
+magic_run_re = re.compile(r'.*(\.ipy|\.ipynb|\.py[w]?)$')
 
 #-----------------------------------------------------------------------------
 # Local utilities
@@ -250,7 +250,7 @@ def module_completer(self,event):
 # completers, that is currently reimplemented in each.
 
 def magic_run_completer(self, event):
-    """Complete files that end in .py or .ipy for the %run command.
+    """Complete files that end in .py or .ipy or .ipynb for the %run command.
     """
     comps = arg_split(event.line, strict=False)
     relpath = (len(comps) > 1 and comps[-1] or '').strip("'\"")
@@ -274,7 +274,7 @@ def magic_run_completer(self, event):
     else:
         pys =  [f.replace('\\','/')
                 for f in lglob(relpath+'*.py') + lglob(relpath+'*.ipy') +
-                lglob(relpath + '*.pyw')]
+                lglob(relpath+'*.ipynb') + lglob(relpath + '*.pyw')]
     #print('run comp:', dirs+pys) # dbg
     return [compress_user(p, tilde_expand, tilde_val) for p in dirs+pys]
 
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -2536,13 +2536,13 @@ def safe_execfile(self, fname, *where, **kw):
                 self.showtraceback()
 
     def safe_execfile_ipy(self, fname):
-        """Like safe_execfile, but for .ipy files with IPython syntax.
+        """Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
 
         Parameters
         ----------
         fname : str
             The name of the file to execute.  The filename must have a
-            .ipy extension.
+            .ipy or .ipynb extension.
         """
         fname = os.path.abspath(os.path.expanduser(fname))
 
@@ -2558,15 +2558,29 @@ def safe_execfile_ipy(self, fname):
         # behavior of running a script from the system command line, where
         # Python inserts the script's directory into sys.path
         dname = os.path.dirname(fname)
+        
+        def get_cells():
+            """generator for sequence of code blocks to run"""
+            if fname.endswith('.ipynb'):
+                from IPython.nbformat import current
+                with open(fname) as f:
+                    nb = current.read(f, 'json')
+                    if not nb.worksheets:
+                        return
+                    for cell in nb.worksheets[0].cells:
+                        yield cell.input
+            else:
+                with open(fname) as f:
+                    yield f.read()
 
         with prepended_to_syspath(dname):
             try:
-                with open(fname) as thefile:
+                for cell in get_cells():
                     # self.run_cell currently captures all exceptions
                     # raised in user code.  It would be nice if there were
-                    # versions of runlines, execfile that did raise, so
+                    # versions of run_cell that did raise, so
                     # we could catch the errors.
-                    self.run_cell(thefile.read(), store_history=False, shell_futures=False)
+                    self.run_cell(cell, store_history=False, shell_futures=False)
             except:
                 self.showtraceback()
                 warn('Unknown failure executing file: <%s>' % fname)
diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py
--- a/IPython/core/magics/execution.py
+++ b/IPython/core/magics/execution.py
@@ -552,7 +552,7 @@ def run(self, parameter_s='', runner=None,
           details on the options available specifically for profiling.
 
         There is one special usage for which the text above doesn't apply:
-        if the filename ends with .ipy, the file is run as ipython script,
+        if the filename ends with .ipy[nb], the file is run as ipython script,
         just as if the commands were written on IPython prompt.
 
         -m
@@ -596,7 +596,7 @@ def run(self, parameter_s='', runner=None,
             error(msg)
             return
 
-        if filename.lower().endswith('.ipy'):
+        if filename.lower().endswith(('.ipy', '.ipynb')):
             with preserve_keys(self.shell.user_ns, '__file__'):
                 self.shell.user_ns['__file__'] = filename
                 self.shell.safe_execfile_ipy(filename)


Test patch:
diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py
--- a/IPython/core/tests/test_run.py
+++ b/IPython/core/tests/test_run.py
@@ -370,6 +370,23 @@ def test_ignore_sys_exit(self):
         
         with tt.AssertNotPrints('SystemExit'):
             _ip.magic('run -e %s' % self.fname)
+    
+    def test_run_nb(self):
+        """Test %run notebook.ipynb"""
+        from IPython.nbformat import current
+        nb = current.new_notebook(
+            worksheets=[
+                current.new_worksheet(cells=[
+                    current.new_code_cell("answer=42")
+                ])
+            ]
+        )
+        src = current.writes(nb, 'json')
+        self.mktmp(src, ext='.ipynb')
+        
+        _ip.magic("run %s" % self.fname)
+        
+        nt.assert_equal(_ip.user_ns['answer'], 42)