Notebook extensions

Notebook extensions, or plugins, allow the end user to highly control the behavior and the appearance of the Notebook application.

Extensions capability can highly vary from being able to load notebook files from Google Drive, or PostGresSql server, presenting the Notebook in the form of Slideshow, or by just adding a convenient button or keyboard shortcut for an action the user is often doing.

The way we write Jupyter/IPython is to provide the minimal sensible default, with easy access to configuration for extensions to modify behavior.

Extensions can be composed of many pieces, but you will mostly find a Javascript part that lives on the frontend side (ie, the Browser, written in Javascript), and a part that lives on the server side (written in Python). We will, for a first time, mostly focus on the Javascript side.

A large number of repos exist here and there on the internet and we haven't taken the time to write a Jupyter Store (yet) to make extensions easily installable. Well I suppose this can be done as an extension, and your research on the web will probably show that it can be done, but we will still focus on the old manual way of installing extensions to learn how things work because that's why you are here right ?

Here are links to some active repos with extensions:

I've made a minimal extension for you in extensions. Link or move it over into:

$ ls ~/.ipython/nbextensions/
hello-scipy.js

Now let's open a notebook and configure it to load the extension automatically. In a new notebook, or the one I provided with a reminder of the instructions, open the developer console and enter the following:

IPython.notebook.config.update({
  "load_extensions": {"hello-scipy":true}
})

Now Reload your page, and observe the Javascript console, it should tell you what to do next !

Explanation

Do not be preoccupied with what IPython.notebook.config.update is. We will see that later.

The "load_extensions" part takes a dict with the name of extensions and whether they are loaded or not. It is one of the config values which is now stored on server side.

There is a way to activate extensions from outside the notebook but we won't use that for now.

The extension

define(function(){

    function _on_load(){
          console.info('Hello SciPy 2015')
    }

    return {load_ipython_extension: _on_load };
})

The define call: define(function(){ suggests we have no dependencies.

For readability we define a function that will be called on notebook load at the right time. We keep the python convention that _xxx indicates a private function.

  function _on_load(){
        console.info('Hello SciPy 2015')
  }

We only export a function called load_ipython_extension to the outside world: return {load_ipython_extension: _on_load };. Anything outside of this dict will be inaccessible for the rest of the code. You can see that as Python's __all__.

Note that you will find legacy extensions on the internet that do not define load_ipython_extension and rely on IPython's Events, and Custom.js. While this does work for the time being, these extensions will break in the future and are subject to race conditions.

While our Javascript API is still highly in motion, and not guaranteed stable, we will try our best to make updating extensions that use load_ipython_extension easier than the ones using Events and custom.js !

New keyboard shortcut !

Now let's modify our extension in order to be able to actually modify the User interface. We will try to create a shortcut that kills the kernel without confirmation, clears all the cell output, and finally re-runs all cells.

First, we want to get access to the IPython instance. To do so we want to import the right module so that the IPython variable can be used safely.

Change the first line to the following

define(['base/js/namespace'],function(IPython){

I remind you that this is basically equivalent to :

import base.js.namespace as IPython

Now in your _on_load you can access IPython.<things>. If you fail to use the above way of declaring import, IPython might still be accessible on your machine with your current workload. Though it might break in some cases. Using define([...]) insures in the dependency graph that the right file is loaded and that the local name will be IPython (hint, in next release the global name might be Jupyter).

Now let's make a detour and Keyboard Shortcut.

A few things you might need :

var internal_name = IPython.keyboard_manager.actions.register(data, name , `scipy-2015`)
IPython.keyboard_manager.command_shortcuts.remove_shortcut(string)
IPython.keyboard_manager.command_shortcuts.add_shortcut(string, internal_name)

The notebook instance has a clear_all_output method, and a kernel attribute. The kernel instance has a restart method that uses on_success and on_error callbacks.

...

have you figured it out ?

My solution:

function (env) {
    var on_success = undefined;
    var on_error = undefined;

    env.notebook.clear_all_output();
    env.notebook.kernel.restart(function(){
          setTimeout(function(){ // wait 1 sec,
              // todo listen on Kernel ready event.
              console.log('executing all cells')
              env.notebook.execute_all_cells()
          }, 1000)
        },
        on_error // Todo also
    );
}
// register our new action
var action_name = IPython.keyboard_manager.actions.register(
      clear_all_cell_restart,
      'clear-all-cells-restart',
      'scipy-2015')

// unbind 00
IPython.keyboard_manager.command_shortcuts.remove_shortcut('0,0')

// bind 000
IPython.keyboard_manager.command_shortcuts.add_shortcut('0,0,0', action_name)

Why use an action?

How are things up until now ? You might feel like the code is a bit too verbose, and that some parts are unnecessary right ? Now we will start to see why we use such verbose methods.

You might have seen that some attributes of actions seem to be unused.

help: 'Clear all cell and restart kernel without confirmations',
icon : 'fa-recycle',
help_index : '',

Now that your extension works go take a look in the help menu, keyboard shortcut submenu. If all is fine, you should see your new shortcut in there, with the help text. The help index is use to order/group the common shortcuts together. The only last unused piece is the icon.

With all these attribute, you can easily bind an action, to either a keyboard shortcut, a button in a toolbar, or in a menu item (api is not there yet for that though). We often saw people wanting the same action in two places, and duplicating code, which is a bit painful. By defining actions separately it is easy to use these in many places keeping it DRY. This also allows you to distribute actions libraries without actually binding them and lets the users do their own key/icon bindings.

Let see that in next section with toolbars.

Toolbars.

This will be pretty simple since you already did all the work :-)

You just need to know that the following exists, and takes a list of action names:

IPython.toolbar.add_buttons_group

Now, go edit your custom extension ! You can also try to install the markcell.js extension, require() it in your extension and try to use some of the methods defined in it. This shows you how to spread your extension potentially across many files.

Here is my solution:

IPython.toolbar.add_buttons_group(['scipy-2015.clear-all-cells-restart','ipython.restart-kernel'])

each call to this API will generate a new group of buttons with the default icons, and if you hover the button the help text will remind you of the action.

Interact with user

You can ask a value with the base/js/dialog module that has some convenience functions.

This module has a modal function that you can use like this:

dialog.modal({
    body: text_or_dom_node , // jQuery is you friend
    title: string,
    buttons: {
      'Ok':{
            class: 'btn-primary',
            click: on_ok_callback
            },
      'Cancel':{
              //... (or nothing to just dismiss )
            }
    },
    notebook:env.notebook,
    keyboard_manager: env.notebook.keyboard_manager,
})

Server side handler.

Ok, enough javascript (for now). Let's get back into a sane language. Notebook extensions on the client-side have been there for quite a while and we recently added the ability to have a server side extension.

Server side extensions are, as any IPython extension, simply Python modules that define a specific method. In our case load_jupyter_server_extension (Yes we are ready for the future).

Here is the minimal extension you can have:

def load_jupyter_server_extension(nbapp):
    pass

I've already provided that for you in the extensions dir. You can try to run the following. If you look at the console while starting the notebook you will be able to see a new login message.

python3 -m IPython  notebook --notebook-dir=~ --NotebookApp.server_extensions="['extensions.server_ext']"

Now let's add a handler capable of preating requests: