Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Jupyterlab Extensions for the Impatient

License

NotificationsYou must be signed in to change notification settings

MMesch/labextension_tutorial

Repository files navigation

Development is now happening inhttps://github.com/jtpio/jupyterlab-extension-examples with the following roadmap:

Other tutorials

Table of Contents

Introduction

This is a short tutorial series about JupyterLab extensions. Jupyterlab can beused as a platform to combine existing data-science components into a newpowerful application that can be deployed remotely for many users. Some of thehigher level components that can be used are text editors, terminals,notebooks, interactive widgets, filebrowser, renderers for different fileformats that provide access to an enormous ecosystem of libraries fromdifferent languages.

Prerequesites

Writing an extension is not particularly difficult but requires very basicknowledge of javascript and typescript and potentially Python.

Don't be scared of typescript, I never coded in typescript before I touchedJupyterLab but found it easier to understand than pure javascript if you have abasic understanding of object oriented programming and types.

1 Hello World: Setting up the development environment

The template folder structure

Writing a JupyterLab extension usually starts from a configurable template. Itcan be downloded with thecookiecutter tool and the following command:

cookiecutter https://github.com/JupyterLab/extension-cookiecutter-ts

cookiecutter asks for some basic information that could for example be setuplike this:

author_name []: tutoextension_name [JupyterLab_myextension]: 1_hello_worldproject_short_description [A JupyterLab extension.]: minimal lab examplerepository [https://github.com/my_name/JupyterLab_myextension]:

The cookiecutter creates the directory1_hello_world [or your extension name]that looks like this:

1_hello_world/├── README.md├── package.json├── tsconfig.json├── src│   └── index.ts└── style    └── index.css
  • README.md contains some instructions
  • package.json contains information about the extension such as dependencies
  • tsconfig.json contains information for the typescript compilation
  • src/index.tsthis contains the actual code of our extension
  • style/index.css contains style elements that we can use

What does this extension do? You don't need a PhD in Computer Science to take aguess from the title of this section, but let's have a closer look:

A minimal extension that prints to the browser console

We start with the filesrc/index.ts. This typescript file contains the mainlogic of the extension. It begins with the following import section:

import{JupyterLab,JupyterLabPlugin}from'@JupyterLab/application';import'../style/index.css';

JupyterLab is class of the main Jupyterlab application. It allows us toaccess and modify some of its main components.JupyterLabPlugin is the classof the extension that we are building. Both classes are imported from a packagecalled@JupyterLab/application. The dependency of our extension on thispackage is declared in the filepackage.json:

"dependencies": {"@JupyterLab/application":"^0.16.0"  },

With this basic import setup, we can move on to construct a new instanceof theJupyterLabPlugin class:

constextension:JupyterLabPlugin<void>={id:'1_hello_world',autoStart:true,activate:(app:JupyterLab)=>{console.log('JupyterLab extension 1_hello_world is activated!');}};exportdefaultextension;

a JupyterLabPlugin contains a few attributes that are fairly self-explanatoryin the case ofid andautoStart. Theactivate attribute links to afunction (() => {} notation) that takes one argumentapp of typeJupyterLab and then calls theconsole.log function that is used to outputsomething into the browser console in javascript.app is simply the mainJupyterLab application. The activate function acts as an entry point into theextension and we will gradually extend it to access and modify functionalitythrough theapp object.

Our newJupyterLabPlugin instance has to be finally exported to be visible toJupyterLab, which is done with the lineexport default extension. This bringsus to the next point. How can we plug this extension into JupyterLab?

Building and Installing an Extension

Let's look at theREADME.md file. It contains instructions how ourlabextension can be installed for development:

For a development install (requires npm version 4 or later), do the followingin the repository directory:

npm installnpm run buildjupyter labextension link.

Roughly the first command installs the dependencies that are specified inpackage.json. Among the dependencies are also all of theJupyterLabcomponents that we want to use in our project, but more about this later. Thesecond step runs the build script. In this step, the typescript code getsconverted to javascript using the compilertsc and stored in alibdirectory. Finally, we link the module to JupyterLab.

After all of these steps are done, runningjupyter labextension list shouldnow show something like:

local extensions:        1_hello_world: [...]/labextension_tutorial/1_hello_world

Now let's check inside of JupyterLab if it works. Run [can take a while]:

jupyter lab --watch

Our extension doesn't do much so far, it just writes something to the browserconsole. So let's check if it worked. In firefox you can open the consolepressing thef12 key. You should see something like:

JupyterLab extension 1_hello_world is activated

Our extension works but it is incredibly boring. Let's modify the source codea bit. Simply replace theactivate function with the following lines:

    activate:(app:JupyterLab)=>{console.log('the main JupyterLab application:');console.log(app);}

to update the module, we simply need to go into the extension directory and runnpm run build again. Since we used the--watch option when startingJupyterLab, we now only have to refresh the JupyterLab website in the browserand should see in the browser console:

Object { _started: true, _pluginMap: {…}, _serviceMap: Map(28), _delegate: {…}, commands: {…}, contextMenu: {…}, shell: {…}, registerPluginErrors: [], _dirtyCount: 0, _info: {…}, … } index.js:12

This is the main application JupyterLab object and we will see how to interactwith it in the next sections.

checkout how the core packages of JupyterLab are defined athttps://github.com/JupyterLab/JupyterLab/tree/master/packages . Each package isstructured similarly to the extension that we are writing. This modularstructure makes JupyterLab very adapatable

An overview of the classes and their attributes and methods can be found in theJupyterLab documentation. The@JupyterLab/application module documentation ishereand which links to theJupyterLab class.TheJupyterLabPlugin is a type alias [a new name] for the typeIPlugin.The definition ofIPlugin is more difficult to find because it is defined bythephosphor.js library that runs JupyterLab under the hood (more about thislater). Its documentation is therefore located on thephosphor.jswebsite.

Click here for the final extension: 1_hello_world

2 Commands and Menus: Extending the main app

For the next extension you can either copy the last folder to a new one orsimply continue modifying it. In case that you want to have a new extension,open the filepackage.json and modify the package name, e.g. into2_commands_and_menus. The same name changes needs to be done insrc/index.ts.

Jupyterlab Commands

Start it withjupyter lab --watch. In this extension, we are going to add acommand to the application command registry and expose it to the user in thecommand palette. The command palette can be seen when clicking onCommandson the left hand side of Jupyterlab. The command palette can be seen as a listof actions that can be executed by JupyterLab. (see screenshot below).

Jupyter Command Registry

Extensions can provide a bunch of functions to the JupyterLab command registryand then expose them to the user through the command palette or through a menuitem.

Two types play a role in this: theCommandRegistry type (documentation)and the command palette interfaceICommandPalette that is imported with:

import{ICommandPalette}from'@JupyterLab/apputils';

To see how we access the applications command registry and command paletteopen the filesrc/index.ts.

constextension:JupyterLabPlugin<void>={id:'2_commands_and_menus',autoStart:true,requires:[ICommandPalette],activate:(app:JupyterLab,palette:ICommandPalette)=>{const{ commands}=app;letcommand='labtutorial';letcategory='Tutorial';commands.addCommand(command,{label:'New Labtutorial',caption:'Open the Labtutorial',execute:(args)=>{console.log('Hey')}});palette.addItem({command, category});}};exportdefaultextension;

The CommandRegistry is an attribute of the main JupyterLab application(variableapp in the previous section). It has anaddCommand method thatadds our own function.The ICommandPalette(documentation)is passed to theactivate function as an argument (variablepalette) inaddition to the JupyterLab application (variableapp). We specify with thepropertyrequires: [ICommandPalette], which additional arguments we want toinject into theactivate function in the JupyterLabPlugin. ICommandPaletteprovides the methodaddItem that links a palette entry to a command in thecommand registry. Our new plugin code then becomes:

When this extension is build (and linked if necessary), JupyterLab looks likethis:

New Command

Adding new Menu tabs and items

Adding new menu items works in a similar way. The IMainMenu interface can bepassed as a new argument two the activate function, but first it has to beimported. The Menu class is imported from the phosphor library on top of whichJupyterLab is built, and that will be frequently encountered when developingJupyterLab extensions:

import{IMainMenu}from'@JupyterLab/mainmenu';import{Menu}from'@phosphor/widgets';

We add the IMainMenu in therequires: property such that it is injected intotheactivate function. The Extension is then changed to:

constextension:JupyterLabPlugin<void>={id:'2_commands_and_menus',autoStart:true,requires:[ICommandPalette,IMainMenu],activate:(app:JupyterLab,palette:ICommandPalette,mainMenu:IMainMenu)=>{const{ commands}=app;letcommand='labtutorial';letcategory='Tutorial';commands.addCommand(command,{label:'New Labtutorial',caption:'Open the Labtutorial',execute:(args)=>{console.log('Hey')}});palette.addItem({command, category});lettutorialMenu:Menu=newMenu({commands});tutorialMenu.title.label='Tutorial';mainMenu.addMenu(tutorialMenu,{rank:80});tutorialMenu.addItem({ command});}};exportdefaultextension;

In this extension, we have added the dependenciesJupyterLab/mainmenu andphosphor/widgets. Before it builds, this dependencies have to be added to thepackage.json file:

"dependencies": {"@JupyterLab/application":"^0.16.0","@JupyterLab/mainmenu":"*","@phosphor/widgets":"*"  }

we can then do

npm installnpm run build

to rebuild the application. After a browser refresh, the JupyterLab websiteshould now show:

New Menu

[ps:

for the build to run, thetsconfig.json file might have to be updated to:

{  "compilerOptions": {    "declaration": true,    "noImplicitAny": true,    "noEmitOnError": true,    "noUnusedLocals": true,    "module": "commonjs",    "moduleResolution": "node",    "target": "ES6",    "outDir": "./lib",    "lib": ["ES6", "ES2015.Promise", "DOM"],    "types": []  },  "include": ["src/*"]}

]

Click here for the final extension 2_commands_and_menus

3 Widgets: Adding new Elements to the Main Window

Finally we are going to do some real stuff and add a new tab to JupyterLab.Visible elements such as a tab are represented by widgets in the phosphorlibrary that is the basis of the JupyterLab application.

A basic tab

The base widget class can be imported with:

import{Widget}from'@phosphor/widgets';

A Widget can be added to the main area through the main JupyterLabapplicationapp.shell. Inside of our previousactivate function, this lookslike this:

    activate: (        app: JupyterLab,        palette: ICommandPalette,        mainMenu: IMainMenu) =>    {        const { commands, shell } = app;        let command = 'ex3:labtutorial';        let category = 'Tutorial';        commands.addCommand(command, {            label: 'Ex3 command',            caption: 'Open the Labtutorial',            execute: (args) => {                const widget = new TutorialView();                shell.addToMainArea(widget);}});        palette.addItem({command, category});        let tutorialMenu: Menu = new Menu({commands});        tutorialMenu.title.label = 'Tutorial';        mainMenu.addMenu(tutorialMenu, {rank: 80});        tutorialMenu.addItem({ command });    }

The custom widgetTutorialView is straight-forward as well:

classTutorialViewextendsWidget{constructor(){super();this.addClass('jp-tutorial-view')this.id='tutorial'this.title.label='Tutorial View'this.title.closable=true;}}

Note that we have used a custom css class that is defined in the filestyle/index.css as:

.jp-tutorial-view {    background-color: AliceBlue;}

Our custom tab can be started in JupyterLab from the command palette and lookslike this:

Custom Tab

Click here for the final extension: 3_widgets

Datagrid: a Fancy Phosphor Widget

Now let's do something a little more advanced. Jupyterlab is build on top ofPhosphor.js. Let's see if we can plugthis phosphor exampleinto JupyterLab.

We start by importing thePanel widget and theDataGrid andDataModelclasses from phosphor:

import{Panel}from'@phosphor/widgets';import{DataGrid,DataModel}from'@phosphor/datagrid';

The Panel widget can hold several sub-widgets that are added with its.addWidget method.DataModel is a class that provides the data that isdisplayed by theDataGrid widget.

With these three classes, we adapt theTutorialView as follows:

classTutorialViewextendsPanel{constructor(){super();this.addClass('jp-tutorial-view')this.id='tutorial'this.title.label='Tutorial View'this.title.closable=true;letmodel=newLargeDataModel();letgrid=newDataGrid();grid.model=model;this.addWidget(grid);}}

That's rather easy. Let's now dive into theDataModel class that is takenfrom the official phosphor.js example. The first few lines look like this:

classLargeDataModelextendsDataModel{rowCount(region:DataModel.RowRegion):number{returnregion==='body' ?1000000000000 :2;}columnCount(region:DataModel.ColumnRegion):number{returnregion==='body' ?1000000000000 :3;}

While it is fairly obvious thatrowCount andcolumnCount are supposedto return some number of rows and columns, it is a little more cryptic whattheRowRegion and theColumnRegion input arguments are. Let's have alook at their definition in the phosphor.js source code:

exporttypeRowRegion='body'|'column-header';/** * A type alias for the data model column regions. */exporttypeColumnRegion='body'|'row-header';/** * A type alias for the data model cell regions. */exporttypeCellRegion='body'|'row-header'|'column-header'|'corner-header';

The meaning of these lines might be obvious for experienced users of typescriptor Haskell. The| can be read as or. This means that theRowRegion type iseitherbody orcolumn-header, explaining what therowCount andcolumnCount functions do: They define a table with2 header rows, with 3index columns, with1000000000000 rows and1000000000000 columns.

The remaining part of the LargeDataModel class defines the data values of thedatagrid. In this case it simply displays the row and column index in eachcell, and adds a letter prefix in case that we are in any of the headerregions:

data(region:DataModel.CellRegion,row:number,column:number): any{if(region==='row-header'){return`R:${row},${column}`;}if(region==='column-header'){return`C:${row},${column}`;}if(region==='corner-header'){return`N:${row},${column}`;}return`(${row},${column})`;}}

Let's see how this looks like in Jupyterlab:

Datagrid

Click here for the final extension: 3b_datagrid

4 Kernel Outputs: Simple Notebook-style Rendering

In this extension we will see how initialize a kernel, and how to execute codeand how to display the rendered output. We use theOutputArea class for thispurpose that Jupyterlab uses internally to render the output area under anotebook cell or the output area in the console.

Essentially,OutputArea will renders the data that comes as a reply to anexecute message that was sent to an underlying kernel. Under the hood, theOutputArea and theOutputAreaModel classes act similar to theKernelViewandKernelModel classes that we have defined ourselves before. We thereforeget rid of themodel.ts andwidget.tsx files and change the panel class to:

Reorganizing the extension code

Since our extension is growing bigger, we begin by splitting our code into moremanagable units. Roughly we can see three larger components of our application:

  1. theJupyterLabPlugin that activates all extension components and connectsthem to the mainJupyterlab application via commands, launcher, or menuitems.
  2. a Panel that combines different widgets into a single application

We split these components in the two files:

src/├── index.ts└── panel.ts

Let's go first throughpanel.ts. This is the full Panel class that displaysstarts a kernel, then sends code to it end displays the returned data with thejupyter renderers:

exportclass TutorialPanel extends StackedPanel {    constructor(manager: ServiceManager.IManager, rendermime: RenderMimeRegistry) {        super();        this.addClass(PANEL_CLASS);        this.id = 'TutorialPanel';        this.title.label = 'Tutorial View'        this.title.closable = true;        let path = './console';        this._session = new ClientSession({            manager: manager.sessions,            path,            name: 'Tutorial',        });        this._outputareamodel = new OutputAreaModel();        this._outputarea = new SimplifiedOutputArea({ model: this._outputareamodel, rendermime: rendermime });        this.addWidget(this._outputarea);        this._session.initialize();    }    dispose(): void {        this._session.dispose();        super.dispose();    }    public execute(code: string) {        SimplifiedOutputArea.execute(code, this._outputarea, this._session)            .then((msg: KernelMessage.IExecuteReplyMsg) => {console.log(msg); })    }    protected onCloseRequest(msg: Message): void {        super.onCloseRequest(msg);        this.dispose();    }    get session(): IClientSession {        return this._session;    }    private _session: ClientSession;    private _outputarea: SimplifiedOutputArea;    private _outputareamodel: OutputAreaModel;}

Initializing a Kernel Session

The first thing that we want to focus on is theClientSession that isstored in the private_session variable:

private _session: ClientSession;

A ClientSession handles a single kernel session. The session itself (not yetthe kernel) is started with these lines:

        let path = './console';        this._session = new ClientSession({            manager: manager.sessions,            path,            name: 'Tutorial',        });

A kernel is initialized with this line:

        this._session.initialize();

In case that a session has no predefined favourite kernel, a popup will bestarted that asks the user which kernel should be used. Conveniently, this canalso be an already existing kernel, as we will see later.

The following three methods add functionality to cleanly dispose of the sessionwhen we close the panel, and to expose the private session variable such thatother users can access it.

    dispose(): void {        this._session.dispose();        super.dispose();    }    protected onCloseRequest(msg: Message): void {        super.onCloseRequest(msg);        this.dispose();    }    get session(): IClientSession {        return this._session;    }

OutputArea and Model

TheSimplifiedOutputArea class is a Widget, as we have seen them before. Wecan instantiate it with a newOutputAreaModel (this is class that containsthe data that will be shown):

        this._outputareamodel = new OutputAreaModel();        this._outputarea = new SimplifiedOutputArea({ model: this._outputareamodel, rendermime: rendermime });

SimplifiedOutputArea provides the classmethodexecute that basically sendssome code to a kernel through a ClientSession and that then displays the resultin a specificSimplifiedOutputArea instance:

    public execute(code: string) {        SimplifiedOutputArea.execute(code, this._outputarea, this._session)            .then((msg: KernelMessage.IExecuteReplyMsg) => {console.log(msg); })    }

TheSimplifiedOutputArea.execute function receives at some point a responsemessage from the kernel which says that the code was executed (this messagedoes not contain the data that is displayed). When this message is received,.then is executed and prints this message to the console.

We just have to add theSimplifiedOutputArea Widget to our Panel with:

        this.addWidget(this._outputarea);

and we are ready to add the whole Panel to Jupyterlab.

asynchronous extension initialization

index.ts is responsible to initialize the extension. We only go throughthe changes with respect to the last sections.

First we reorganize the extension commands into one unified namespace:

namespaceCommandIDs{exportconstcreate='Ex7:create';exportconstexecute='Ex7:execute';}

This allows us to add commands from the command registry to the pallette andmenu tab in a single call:

// add items in command palette and menu[CommandIDs.create,CommandIDs.execute].forEach(command=>{palette.addItem({ command, category});tutorialMenu.addItem({ command});});

Another change is that we now use themanager to add our extension after theother jupyter services are ready. The serviceManager can be obtained from themain application as:

constmanager=app.serviceManager;

to launch our application, we can then use:

letpanel:TutorialPanel;functioncreatePanel(){returnmanager.ready.then(()=>{panel=newTutorialPanel(manager,rendermime);returnpanel.session.ready}).then(()=>{shell.addToMainArea(panel);returnpanel});}

Make it Run

Let's for example display the variabledf from a python kernel that couldcontain a pandas dataframe. To do this, we just need to add a command to thecommand registry inindex.ts

    command = CommandIDs.execute    commands.addCommand(command, {        label: 'Ex7: show dataframe',        caption: 'show dataframe',        execute: (args) => {panel.execute('df')}});

and we are ready to see it. The final extension looks like this:

OutputArea class

Click here for the final extension: 4a_outputareas

Jupyter-Widgets: Adding Interactive Elements

A lot of advanced functionality in Jupyter notebooks comes in the form ofjupyter widgets (ipython widgets). Jupyter widgets are elements that have onerepresentation in a kernel and another representation in the JupyterLabfrontend code. Imagine having a large dataset in the kernel that we want toexamine and modify interactively. In this case, the frontend part of the widgetcan be changed and updates synchronously the kernel data. Many other widgettypes are available and can give an app-like feeling to a Jupyter notebook.These widgets are therefore ideal component of a JupyterLab extension.

We show in this extension how the ipywidgetqgrid can be integrated intoJupyterLab. As a first step, installipywidgets andgrid. It should workin a similar way with any other ipywidget.

(These are the commands to install the ipywidgets with anaconda:

conda install -c conda-forge ipywidgetsconda install -c conda-forge qgridjupyter labextension install @jupyter-widgets/JupyterLab-managerjupyter labextension install qgrid

)

Before continuing, test if you can (a) open a notebook, and (b) see a tablewhen executing these commands in a cell:

import pandas as pdimport numpy as npimport qgriddf = pd.DataFrame(np.arange(25).reshape(5, 5))qgrid.show_grid(df)

If yes, we can check out how we can include this table in our own app. Similarto the previous Extension, we will use theOutputArea class to display thewidget. Only some minor adjustments have to be made to make it work.

The first thing is to understand the nature of the jupyter-widgets JupyterLabextension (calledjupyterlab-manager). As this text is written (26/6/2018) itis adocument extension and not a general extension to JupyterLab. This meansit provides extra functionality to the notebook document type and not the thefull Jupyterlab app. The relevant lines from the jupyter-widgets source codethat show how it registers its renderer with Jupyterlab are the following:

exportclassNBWidgetExtensionimplementsINBWidgetExtension{/**   * Create a new extension object.   */createNew(nb:NotebookPanel,context:DocumentRegistry.IContext<INotebookModel>):IDisposable{letwManager=newWidgetManager(context,nb.rendermime);this._registry.forEach(data=>wManager.register(data));nb.rendermime.addFactory({safe:false,mimeTypes:[WIDGET_MIMETYPE],createRenderer:(options)=>newWidgetRenderer(options,wManager)},0);returnnewDisposableDelegate(()=>{if(nb.rendermime){nb.rendermime.removeMimeType(WIDGET_MIMETYPE);}wManager.dispose();});}/**   * Register a widget module.   */registerWidget(data:base.IWidgetRegistryData){this._registry.push(data);}private_registry:base.IWidgetRegistryData[]=[];}

ThecreateNew method ofNBWidgetExtension takes aNotebookPanel as inputargument and then adds a custom mime renderer with the commandnb.rendermime.addFactory to it. The widget renderer (or rather RenderFactory)is linked to theWidgetManager that stores the underlying data of thejupyter-widgets. Unfortunately, this means that we have to accessjupyter-widgets through a notebook because its renderer and WidgetManager areattached to it.

To access the current notebook, we can use anINotebookTracker in theplugin's activate function:

const extension: JupyterLabPlugin<void> = {    id: '6_ipywidgets',    autoStart: true,    requires: [ICommandPalette, INotebookTracker, ILauncher, IMainMenu],    activate: activate};function activate(    app: JupyterLab,    palette: ICommandPalette,    tracker: INotebookTracker,    launcher: ILauncher,    mainMenu: IMainMenu){    [...]

We then pass therendermime registry of the notebook (the one that has thejupyter-widgets renderer added) to our panel:

    function createPanel() {        let current = tracker.currentWidget;        console.log(current.rendermime);        return manager.ready            .then(() => {                panel = new TutorialPanel(manager, current.rendermime);                return panel.session.ready})            .then(() => {                shell.addToMainArea(panel);                return panel});    }

Finally we add a command to the registry that executes the codewidget thatdisplays the variablewidget in which we are going to store the qgrid widget:

    let code = 'widget'    command = CommandIDs.execute    commands.addCommand(command, {        label: 'Ex8: show widget',        caption: 'show ipython widget',        execute: () => {panel.execute(code)}});

To render the Output we have to allow theOutputAreaModel to use non-defaultmime types, which can be done like this:

        this._outputareamodel = new OutputAreaModel({ trusted: true });

The final output looks is demonstrated in the gif below. We also show that wecan attach a console to a kernel, that shows all executed commands, includingthe one that we send from our Extension.

Qgrid widget

Click here for the final extension: 4b_jupyterwidgets

5 Buttons and Signals: Interactions Between Different Widgets

Phosphor Signaling 101

In this extension, we are going to add some simple buttons to the widget thattrigger the panel to print something to the console. Communication betweendifferent components of JupyterLab are a key ingredient in building anextension. Jupyterlab's phosphor engine uses theISignal interface and theSignal class that implements this interface for communication(documentation).

The basic concept is the following: A widget, in our case the one that containssome visual elements such as a button, defines a signal and exposes it to otherwidgets, as this_stateChanged signal:

getstateChanged():ISignal<TutorialView,void>{returnthis._stateChanged;}private_stateChanged=newSignal<TutorialView,void>(this);

Another widget, in our case the panel that boxes several different widgets,subscribes to thestateChanged signal and links some function to it:

[...].stateChanged.connect(()=>{console.log('changed');});

The function is executed when the signal is triggered with

_stateChanged.emit(void0)

Let's see how we can implement this ...

A simple react button

We start with a file calledsrc/widget.tsx. Thetsx extension allows to useXML-like syntax with the tag notation<>to represent some visual elements(note that you might have to add a line:"jsx": "react", to thetsconfig.json file).

widget.tsx contains one major classTutorialView that extends theVDomRendered class that is provided by Jupyterlab.VDomRenderer defines arender() method that defines some html elements (react) such as a button.

TutorialView further contains a private variablestateChanged of typeSignal. A signal object can be triggered and then emits an actual message.Other Widgets can subscribe to such a signal and react when a message isemitted. We configure one of the buttonsonClick event to trigger thestateChangedsignal with_stateChanged.emit(void 0)`:

exportclassTutorialViewextendsVDomRenderer<any>{constructor(){super();this.id=`TutorialVDOM`}protectedrender():React.ReactElement<any>[]{constelements:React.ReactElement<any>[]=[];elements.push(<buttonkey='header-thread'className="jp-tutorial-button"onClick={()=>{this._stateChanged.emit(void0)}}>Clickme</button>);returnelements;}getstateChanged():ISignal<TutorialView,void>{returnthis._stateChanged;}private_stateChanged=newSignal<TutorialView,void>(this);}

subscribing to a signal

Thepanel.ts class defines an extension panel that displays theTutorialView widget and that subscribes to itsstateChanged signal.Subscription to a signal is done using theconnect method of thestateChanged attribute. It registers a function (in this case() => { console.log('changed'); } that is triggered when the signal isemitted:

exportclassTutorialPanelextendsStackedPanel{constructor(){super();this.addClass(PANEL_CLASS);this.id='TutorialPanel';this.title.label='Tutorial View'this.title.closable=true;this.tutorial=newTutorialView();this.addWidget(this.tutorial);this.tutorial.stateChanged.connect(()=>{console.log('changed');});}privatetutorial:TutorialView;}

The final extension writes a littlechanged text to the browser console whena big red button is clicked. It is not very spectacular but the signaling isconceptualy important for building extensions. It looks like this:

Button with Signal

Click here for the final extension: 5_signals_and_buttons

6 Custom Kernel Interactions: Kernel Managment and Messaging

One of the main features of JupyterLab is the possibility to manage andinteract underlying compute kernels. In this section, we explore how tostart a kernel and execute a simple command on it.

Component Overview

In terms of organization of this app, we want to have these components:

  • index.ts: a JupyterLabPlugin that initializes the plugin and registers commands, menu tabs and a launcher
  • panel.ts: a panel class that is responsible to initialize and hold the kernel session, widgets and models
  • model.ts: a KernelModel class that is responsible to execute code on the kernel and to store the execution result
  • widget.tsx: a KernelView class that is responsible to provide visual elements that trigger the kernel model and display its results

TheKernelView displays theKernelModel with some react html elements andneeds to get updated whenKernelModel changes state, i.e. retrieves a newexecution result. Jupyterlab provides a two class model for such classes,aVDomRendered that has a link to aVDomModel and arender function.TheVDomRendered listens to astateChanged signal that is defined by theVDomModel. Whenever thestateChanged signal is emitted, theVDomRendered calls itsrender function again and updates the html elementsaccording to the new state of the model.

Initializing and managing a kernel session (panel.ts)

Jupyterlab provides a classClientSession(documentation)that manages a single kernel session. Here are the lines that we need to starta kernel with it:

        this._session = new ClientSession({            manager: manager.sessions,            path,            name: 'Tutorial',        });        this._session.initialize();

well, that's short, isn't it? We have already seen themanager class that isprovided directly by the main JupyterLab application.path is a link to thepath under which the console is opened (?).

With these lines, we can extend the panel widget from 7_signals_and_buttons to intialize akernel. In addition, we will initialize aKernelModel class in it andoverwrite thedispose andonCloseRequest methods of theStackedPanel(documentation)to free the kernel session resources if the panel is closed. The whole adaptedpanel class looks like this:

exportclass TutorialPanel extends StackedPanel {    constructor(manager: ServiceManager.IManager) {        super();        this.addClass(PANEL_CLASS);        this.id = 'TutorialPanel';        this.title.label = 'Tutorial View'        this.title.closable = true;        let path = './console';        this._session = new ClientSession({            manager: manager.sessions,            path,            name: 'Tutorial',        });        this._model = new KernelModel(this._session);        this._tutorial = new TutorialView(this._model);        this.addWidget(this._tutorial);        this._session.initialize();    }    dispose(): void {        this._session.dispose();        super.dispose();    }    protected onCloseRequest(msg: Message): void {        super.onCloseRequest(msg);        this.dispose();    }    get session(): IClientSession {        return this._session;    }    private _model: KernelModel;    private _session: ClientSession;    private _tutorial: TutorialView;}

Executing code and retrieving messages from a kernel (model.ts)

Once a kernel is initialized and ready, code can be executed on it throughtheClientSession class with the following snippet:

        this.future = this._session.kernel.requestExecute({ code });

Without getting too much into the details of what thisfuture is, let's thinkabout it as an object that can receive some messages from the kernel as ananswer on our execution request (seejupyter messaging).One of these messages contains the data of the execution result. It ispublished on a channel calledIOPub and can be identified by the messagetypesexecute_result,display_data andupdate_display_data.

Once such a message is received by thefuture object, it can trigger anaction. In our case, we just store this message inthis._output and thenemit astateChanged signal. As we have explained above, ourKernelModel isaVDomModel that provides thisstateChanged signal that can be used by aVDomRendered. It is implemented as follows:

exportclass KernelModel extends VDomModel {    constructor(session: IClientSession) {        super();        this._session = session;    }    public execute(code: string) {        this.future = this._session.kernel.requestExecute({ code });    }    private _onIOPub = (msg: KernelMessage.IIOPubMessage) => {        let msgType = msg.header.msg_type;        switch (msgType) {            case 'execute_result':            case 'display_data':            case 'update_display_data':                this._output = msg.content as nbformat.IOutput;                console.log(this._output);                this.stateChanged.emit(undefined);            default:                break;        }        return true    }    get output(): nbformat.IOutput {        return this._output;    }    get future(): Kernel.IFuture {        return this._future;    }    set future(value: Kernel.IFuture) {        this._future = value;        value.onIOPub = this._onIOPub;    }    private _output: nbformat.IOutput = null;    private _future: Kernel.IFuture = null;    private _session: IClientSession;}

Connecting a View to the Kernel

The only remaining thing left is to connect a View to the Model. We havealready seen theTutorialView before. To trigger therender function of aVDomRendered on astateChanged signal, we just need to add ourVDomModeltothis.model in the constructor. We can then connect a button tothis.model.execute and a text field tothis.model.output and our extensionis ready:

exportclass KernelView extends VDomRenderer<any> {    constructor(model: KernelModel) {        super();        this.id = `TutorialVDOM`        this.model = model    }    protected render(): React.ReactElement<any>[] {        console.log('render');        const elements: React.ReactElement<any>[] = [];        elements.push(            <button key='header-thread'            className="jp-tutorial-button"            onClick={() => {this.model.execute('3+5')}}>            Compute 3+5</button>,            <span key='output field'>{JSON.stringify(this.model.output)}</span>        );        return elements;    }}

How does it look like

Kernel Execution

Well that's nice, the basics are clear, but what about this weird outputobject? In the next extensions, we will explore how we can reuse some jupytercomponents to make things look nicer...

Click here for the final extension: 6_kernel_messages

About

Jupyterlab Extensions for the Impatient

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors2

  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp