IPython Notebook Duck-Punching

If it walks like a duck and talks like a duck, it’s a duck. So if this duck is not giving you the noise that you want, you’ve got to just punch that duck until it returns what you expect.

Small blogpost to answer to one question that has been asked on stackoverflow and on the Issue tracker :

When I open a saved IPython Notebook, I need to evaluate all the cells with imports, function definitions etc. to continue working on the session. It is convenient to click Cell > Run All to do this. But what If I do not want to re-evaluate all calculations? Do I need to pick the cells to evaluate by hand each time?

[There is] the concept of "initialization cells". You can mark some cells in the notebook as initialization cell, and then perform "evaluate initialization cells" after opening the notebook.

This feature should allow certain cells to be marked as Initialization Cells and be evaluated together with the appropriate command.

I'll let you get there to read the official answer, but in short : Not in core IPython, this can be done as an extension.

Some warning before we start. As long as you don't shut down the IPython webserver, or don't ask for explicit shutdown, a notebook kernel stay alive, meaning that you can leave the page and come back, you wil still have the same active namespace. So you might not want to run those init cell again.

Second do not ever, in any case, whatever reason you have, automatically run initialisation cell on notebook load. It is a security risk : If you have such an extension and I send you a notebook with a Initialisation Cell that have rm -rf ~/ in it. You just lost your home folder when openning this notebook.

So this will show you how to to that in less than 60 lines of javascript, you might want to take a look at previous post for some info.

You know where to find the finished code, as usual it is not perfect, and I wait for PR to fix the edge cases. Let's start.

Overview

This extension will be in two part even the all thing will fit in one file. So let's decompose what we need.

  • The ability to mark a cell with a certain flag (Initialisation Cell)
  • The ability run all those initialisation cell when needed

For me this sound like the need for a custom CellToolbar checkbox, and a Toolbar button 'Run init Cell'. A checkbox because this is typically a Boolean statement, the cell is a Initialisation cell, or not. And the toolbar button is the easiest to reach, and not too complicated to add.

The CellToolbar checkbox

We will store the boolean of wether of not the cell is an Initialisation Cell in the cell Metadata. To stay clean, we will use a prefixed key to avoid future collision. As this is a draft, I will prefix the name of the key with an underscore to warn future user that directly accessing this key is not supported and that I reserved myself the right to change anything without warning.

I will store [true|false|undefined] in cell.metadata._draft.init_cell. I will not forget to check that cell.metadata._draft exist before playing with it.

The IPython notebook provide a convenient API to generate checkbox in Cell Toolbar. to use this we need to define a getter and a setter for our metadata.

The setter take the cell we act on, and the new value:

function(cell, value){
     // we check that the _draft namespace exist and create it if needed
     if (cell.metadata._draft == undefined){cell.metadata._draft = {}}
        // set the value
        cell.metadata._draft.init_cell = value
     }

The getter is not much more complicated :

 function(cell){
     var ns = cell.metadata._draft;
     // if the _draft namespace does not exist return undefined
     // (will be interpreted as false by checkbox) otherwise
     // return the value
     return (ns == undefined)? undefined: ns.init_cell
     }

The api to generate the checkbox has the following signature : CellToolbar.utils.checkbox_ui_generator(label, setter, getter) I can then create my function easily:

var CellToolbar= IPython.CellToolbar;


var init_cell = CellToolbar.utils.checkbox_ui_generator('Initialisation Cell',
 // setter
 function(cell, value){
     // we check that the _draft namespace exist and create it if needed
     if (cell.metadata._draft == undefined){cell.metadata._draft = {}}
        // set the value
        cell.metadata._draft.init_cell = value
     },
 //getter
 function(cell){ var ns = cell.metadata._draft;
     // if the _draft namespace does not exist return undefined
     // (will be interpreted as false by checkbox) otherwise
     // return the value
     return (ns == undefined)? undefined: ns.init_cell
     }
 );

The label will be use in the UI to put a name in front of the checkbox to know its use. So I used a descriptive name.

Now we need to register our function, for that we will use CellToolbar.register_callback(name, function);. name should be a string we will use to refer to the function later, in order to use it in multiple place if we wish. Here simply

CellToolbar.register_callback('init_cell.chkb', init_cell);

And finaly, we use a private method (for now) to generate a CellToolbar preset that can be chosen by the User in the CellToolbar dropdown with : CellToolbar.register_preset(label, ['callback_name','callback_name',....]). This allow to simply mix and match ui elements from different preset for customisation. Here we only have one checkbox so we do:

CellToolbar.register_preset('Initialisation Cell', ['init_cell.chkb']);

With all the extension I have, I could create a custom CellToolbar simply by adding:

CellToolbar.register_preset('My toolbar', ['init_cell.chkb','default.rawedit','slideshow.select']);

And you can see below how it looks like

In [19]:
from IPython.display import Image
Image(filename='/Users/bussonniermatthias/Desktop/Ctoolbar.png')
Out[19]:

Now you just need to select the Initiallisation Cell CellToolbar and check the checkbox you wish.

The Toolbar button

Now we need a way to run all cells marked as Initialisation Cells. Let's first make a function that loop on all cell and run is they are marked:

var run_init = function(){

    var cells = IPython.notebook.get_cells();

    for(var i in cells){
        var cell = cells[i];
        var namespace =  cell.metadata._draft|| {};
        var isInit = namespace.init_cell;
        // you also need to check that cell is instance of code cell,
        // but lets keep it short
        if( isInit === true){
            cell.execute();
        }
    }
};

Now we use the API to create a function that register a callback on a button click on the main toolbar, we use a descriptive label that shows on hower, on Icon from jQuery UI themeroller, and assign the callback to previous defined function:

 var add_run_init_button = function(){
    IPython.toolbar.add_buttons_group([
         {
          'label'   : 'run init_cell',
          'icon'    : 'ui-icon-calculator', 
          'callback': run_init
         }
     ]);
    };

Now we just need to run this function late enough to have effect. We will just listen for the loaded even to trigger this :

$([IPython.events]).on('notebook_loaded.Notebook',add_run_init_button);

That's it.

You just have to put all this stuff in one big file, name it correctly and add the $.getScript in your custom.js. Reload your page/Restart your server (depending on the config you choose). And you should have a new toolbar preset and a button to run your initialisation cell. I've got less than 60 lines of javascript comment and blank lines counted.

As this is a separated file, you can easily fork it and add modifications/options. I'm waiting for PRs.

I hope this show you that writing extension for the IPython notebook is not that hard, and are easy to share.

In []:
 
Back to top