How can I programatically add cells to and IPython or Jupyter notebook?

Viewed 920

This question has been asked a few times before, however the accepted answers suggest that the asker was looking to do something different to me.

I am trying to create an IPython widget that creates and runs a new cell. The purpose of this is to provide a similitude between the widget and the IPython cells.

For example: I would like to have a SelectorWidget that fills and runs the next cell with 'cell magic'.

But I am facing the problem that (please correct me if I am wrong) IPython does not keep an internal data-structure of cells.

It seems to me that the only way to modify a new cell is with:

In [1]: get_ipython().set_next_input('1')

In [2]: 1

I'd just like to be able to run that line automatically. But I don't think that's possible with set_next_input given it's just a hack on top of readline.


I can do what I want to do with:

 display(Javacript('''
 
 var code = IPython.notebook.insert_cell_at_bottom('code'))
 code.execute()

 '''))

But it just feels so incredibly wrong to be driving input that way as it depends on a web-browser to do something quite simple. And you're depending on an external event loop.

The equivalent to doing this on the command line would be starting a background thread/process/corountine that has access to the IPython prompt's stdin and that would accept tasks from the foreground thread to write to the forground thread's own stdin. It's possible to do but has the same problem as the Jupyter methodology because it's specific to the input mechanism of IPython.

2 Answers

Looking at the problem, I can't seem to find a fully synchronous method without JavaScript either.

I have found the run_cell and run_cell_magic work to avoid the JavaScript example but fail on producing the cell with the outputs.

The closest I could come to was:

from IPython import get_ipython

def make_cell_run_code(code):
    ipython = get_ipython()
    ipython.set_next_input(code)
    ipython.run_cell(code)

code = "print('test')"
make_cell_run_code(code)

which avoids the JavaScript but still has cells out of sync.

With JavaScript, we can use negative indexing to ensure the same cell is excecuted.

from IPython.display import Javascript, display

def create_and_excecute_code_cell(code=''):
    display(Javascript("""
        var code = IPython.notebook.insert_cell_at_bottom('code');
        code.set_text("{0}");
        Jupyter.notebook.execute_cells([-1]);
    """.format(code)))

create_and_excecute_code_cell("%time x=0")

Which I cribbed from here. Your JavaScript snippet didn't work for me.

Other avenues are to look deeper into IPython.core.interactiveshell.InteractiveShell

One possible but not portable solution is to monkey-patch the JavaScript that handles the set_next_input kernel message.

(function (){
    IPython.CodeCell.prototype._handle_set_next_input = function (payload) {
        var data = {
            cell: this,
            text: payload.text,
            replace: payload.replace,
            clear_output: payload.clear_output,
        execute: payload.execute
        };
        this.events.trigger('set_next_input.Notebook', data);
    };

    var that = IPython.notebook;
    // Out with the old, in with the new
    that.events.unbind("set_next_input")
    that.events.on('set_next_input.Notebook', function (event, data) {
            if (data.replace) {
                data.cell.set_text(data.text);
                if (data.clear_output !== false) {
                  // default (undefined) is true to preserve prior behavior
                  data.cell.clear_output();
                }
            } else {
                var index = that.find_cell_index(data.cell);
                var new_cell = that.insert_cell_below('code',index);
                new_cell.set_text(data.text);
            }
        
        if (data.execute && data.execute === true) {
        new_cell.execute();
        } else {
        that.dirty = true;
        }
    });
})()

This can be then called in the notebook like so:

with open('run_next_cell_patch.js') as f:
    # Monkey patch Jupyter with
    # set_next_input(run=True) functionality
    display(Javascript(f.read()))


from IPython import get_ipython
ip = get_ipython()
ip.payload_manager.write_payload(
        dict(
            source='set_next_input',
            text='print(1)',
            replace=False,
            execute=True
        )

)

That will create a new cell below with print(1) and it's output executed. This is a bit better than inserting JavaScript each time to take control of the input.

Problematically this only works on Jupyter notebook. Still looking for a solution for Jupyter Lab. On the plus side, I think this could be fairly simple to solve in the plain terminal.

Related