Notebook: How to decorate a Notebook function?

Created on 21 Mar 2017  路  8Comments  路  Source: jupyter/notebook

I have a front end extension which has the following code:

define(
    [
        'base/js/namespace'
    ],
    function(Jupyter) {
        function load_ipython_extension() {
            var original_execute_cells = Jupyter.notebook.execute_cells;
            function alternative_execute_cells(cell_indices) {
                console.log('EXTENSION: running decorated execute_cells function');
            }
            Jupyter.notebook.execute_cells = alternative_execute_cells;
        }

        return {
            load_ipython_extension: load_ipython_extension
        };
    }
);

It is supposed to log something to the console every time I run cells. However, it does not do that and cells are run as usual. I had a look at this stackoverflow post, but I cannot find what I am doing wrong.

Is there some special logic, which causes this? Or is it a reference by value issue, so that I only modify the Jupyter object I have in the extension, but not the actual one?

All 8 comments

@ZelphirKaltstahl I think you need to modify Jupyter.Notebook.prototype.execute_cells to be able to modify the base value. That's what I do when I override functions in NotebookList anyway and it seems to work

@louise-davies Thanks! That works. At first I had a typo I did not see.

It tells me that:

accessing `Notebook` is deprecated. Use `require("notebook/js/notebook").Notebook`

But when I replace Jupyter with: require("notebook/js/notebook").Notebook, it stops working again. So I guess I'll have to stick with the deprecated way?

I've noticed, that the problem seems to be a bit more difficult than I thought.

I tried overwriting the function and the way @louise-davies suggested works. Except that in the original function this is used:

Notebook.prototype.execute_cells = function (indices) {
        if (indices.length === 0) {
            return;
        }

        var cell;
        for (var i = 0; i < indices.length; i++) {
            cell = this.get_cell(indices[i]);
            cell.execute();
        }

        this.select(indices[indices.length - 1]);
        this.command_mode();
        this.set_dirty(true);
    };

And when my own code runs and calls the original function as a last step:

return original_execute_cells(cell_indices);

The value of that functions this seems to be not available and I get the following TypeError:

TypeError: this is undefined[Learn More]  main.min.js:27578:13

Which is the first line of the original function, which does anything with this.

So I guess I somehow have to make it so that the this still remains defined. How would I do this?

I'm not an expert, but I think what you need is bind(). It would work something like this:

return original_execute_cells.bind(this)(cell_indices);

@ZelphirKaltstahl Ah yes, I forgot that the deprecation warning would pop up. However, when I use

require("notebook/js/notebook").Notebook.prototype.execute_cells

It seems to work for me?

As for the troubles with this, @takluyver suggestion to use bind is correct. I was hasty when posting my original reply and I forgot to mention that it would be required.

Thanks for the great help!

I think it has to be:

return original_execute_cells.bind(Jupyter.notebook)(cell_indices);

In my case, as I want the original this and not the one of my current scope. Also the:

require("notebook/js/notebook").Notebook...

Seems to work now. Not sure what was wrong before. Maybe only an only inspector window and I got confused or something like that.

Great, I learned something more about JS today and I have a way to decorate any notebook function. In the other issue @takluyver already showed me how to use events though and I think that might be a cleaner way to do it, in cases where running the original function has the same meaning as firing the event. However, when a specific function does not fire such event, this can still be very useful, I think.

If there are hooks you'd like to add where there isn't currently an event, feel free to open an issue (or a PR ;-) to suggest extra events.

Yes, using events seems like the right solution for this:

define(['base/js/events'], function(events) {
    function load_ipython_extension() {
        events.on('execute.CodeCell', function (event) {
            var cell = event.cell;
            console.log('EXTENSION: running decorated execute_cells function');
        });
    }
    return {
        load_ipython_extension: load_ipython_extension
    };
});
Was this page helpful?
0 / 5 - 0 ratings