jupyterの拡張機能を作ってみた件
はじめに
jupyterをよく使っているのですが,たまに「こんな機能あったらな~」と思うことがあります。
「なら自分で作ればいいじゃない」ということで,jupyterの拡張機能を作ってみました。
jupyterの拡張機能はフロントエンドからサーバーの処理まで広くカバーできるようですが,今回は比較的簡単なフロントエンド処理にとどめて実装を行いました。
実装についてはこの動画が非常に参考になりました。
作成したもの
セルの実行が終わったときに音を鳴らして知らせてくれる拡張機能です。
実行が終わるのを待つ時間が勿体ないので,スマホを見たり他の作業をしたりしているのですが,いつの間にか実行が終わってて悲しくなります。そんな人間を救いたい。
もちろんON/OFFが可能で,ONには2種類のモードがあります。
- 単一セル通知モード
セルが未実行の場合にボタンを押すとこのモードがONになり,ボタンが青色になります。
このモードでは,次に実行したセルの処理が終わった際に音を鳴らします。
- 複数セル通知モード
セルを複数実行中の場合にボタンを押すとこのモードがONになり,ボタンがオレンジ色になります。このモードでは,現在実行しているセルの処理が全て終わった際に音を鳴らします。
一応リリースもしているので,以下のコマンドでインストールも可能です。
pip install cell-executed-alert
作成方法
typescriptのjupyter拡張機能サンプルが公開されているので,こちらをベースに作成しました。
jupyterlab>=3.0,node,npm,git,cookiecutter,jupyter_contrib_nbextensions
などが必要になるので,事前にインストールしておくことをお勧めします。
まずはcookiecutterで拡張機能のプロジェクトを作成します。
cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts
この際,色々入力が求められますが,labextension_name(拡張機能名)だけ埋めれば後はEnterで問題ありません。
入力後,拡張機能名のフォルダが作成されます。このフォルダ内で拡張機能を作っていきます。
myextension
├─.github
│ └─workflows
├─myextension
├─src
└─style
srcフォルダ内でtypescriptを,style内でcssを記述していけばOKです。
今回は以下の3つのファイルに変更を加えました。
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import {INotebookTracker} from "@jupyterlab/notebook";
// button.tsからButtonExtensionクラスをimport
import {ButtonExtension} from "./button";
const plugin: JupyterFrontEndPlugin<void> = {
id: 'cell-executed-alert:plugin',
autoStart: true,
activate: (app: JupyterFrontEnd, tracker: INotebookTracker) => {
let buttonExtension = new ButtonExtension();
app.docRegistry.addWidgetExtension('Notebook', buttonExtension);
}
};
export default plugin;
import {ToolbarButton} from "@jupyterlab/apputils";
import {DocumentRegistry} from "@jupyterlab/docregistry";
import {NotebookActions, INotebookModel, NotebookPanel} from "@jupyterlab/notebook";
import {IDisposable} from "@lumino/disposable";
export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
// 単一セル通知モードのフラグ
public flag: boolean;
// 複数セル通知モードのフラグ
public all_flag: boolean;
constructor() {
this.flag = false;
this.all_flag = false;
}
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
//base64コマンドで生成した音声ファイル。""の中に音声をエンコードした文字列が入るが長いので省略。
const base64 = ""
//Audioオブジェクトを生成
const audio = new Audio("data:audio/wav;base64," + base64);
const mybutton = new ToolbarButton({
//label: 'Alert',
className: 'alertButton',
iconClass: 'fa fa-bell',
// クリック時の処理を記述
onClick: () => {
// アラートがOFFの場合
if(this.flag==false && this.all_flag==false){
let cells = document!.querySelectorAll('.jp-InputArea-prompt');
let execution_count = 0;
for (const cell of cells) {
// 現在実行中のセルの数をカウント
if(cell!.innerHTML == '[*]:'){
execution_count += 1;
}
}
// 複数のセルが実行中の場合
// CSSを変更(オレンジ)し,複数セル通知モードON
if(execution_count > 1){
document!.querySelector('.alertButton')!.classList!.replace('alertButton', 'alertButton-all-active');
this.all_flag = true;
}
// 1つのセルが実行中,もしくは実行中のセルがない場合
// CSSを変更(青色)し,単一セル通知モードON
else{
document!.querySelector('.alertButton')!.classList!.replace('alertButton', 'alertButton-active');
this.flag = true;
}
// セルの実行が終わった際に呼び出される関数
NotebookActions.executed.connect((_, action) => {
// 単一セル通知モード:CSSを元に戻し,単一セル通知モードOFF
if(this.flag==true){
audio.play();
//alert('セルの実行が終了しました');
this.flag = false;
document!.querySelector('.alertButton-active')!.classList!.replace('alertButton-active', 'alertButton');
}
// 複数セル通知モード:実行中のセルがなければCSSを元に戻し,複数セル通知モードOFF
if(this.all_flag==true){
let cells = document!.querySelectorAll('.jp-InputArea-prompt');
let execution_count = 0;
for (const cell of cells) {
if(cell!.innerHTML == '[*]:'){
execution_count += 1;
}
}
if(execution_count == 0){
audio.play();
//alert('セルの実行が終了しました');
this.all_flag = false;
document!.querySelector('.alertButton-all-active')!.classList!.replace('alertButton-all-active', 'alertButton');
}
}
});
}else{
// アラートがONの場合にもう一度押すとOFFにする
if(this.flag){
this.flag = false;
document!.querySelector('.alertButton-active')!.classList!.replace('alertButton-active', 'alertButton');
}
if(this.all_flag){
this.all_flag = false;
document!.querySelector('.alertButton-all-active')!.classList!.replace('alertButton-all-active', 'alertButton');
}
NotebookActions.executed.connect((_, action) => {
});
}
},
tooltip: 'Alert'
});
// ツールバーにボタンを追加
panel.toolbar.insertItem(10, 'alertButton', mybutton);
return mybutton;
}
}
.alertButton.jp-Button.minimal .jp-Icon {
color: gray;
}
.alertButton-active.jp-Button.minimal .jp-Icon {
color: #39a9d6;
}
.alertButton-all-active.jp-Button.minimal .jp-Icon {
color: #f37726;
}
// 例)import {INotebookTracker} from "@jupyterlab/notebook";を記述した場合
jlpm add @jupyterlab/notebook
拡張機能に必要なファイルを一通り作り終えたら,拡張機能をbuildします。初回のbuildは以下のコマンドを続けて実行します。
// lockファイルの生成
jlpm
// 拡張機能のbuild
jlpm build
無事buildに成功すると,拡張機能をjupyter labにインストールすることができるようになります。
jupyter labextension link .
jupyter labextension develop --overwrite .
これで拡張機能は完成です。jupyter labを立ち上げて確認してみましょう。
また,以下のコマンドで自分が作成した拡張機能がインストールされていることを確認できます。
jupyter labextension list
おわりに
jupyterの拡張機能を作ってみよう!と思い立ったものの,日本語の記事でまるっと参考にできそうなものはなく,公式ドキュメントを漁ろうにも情報が整理されていない印象でした(自分が探しきれていないだけかもしれませんが)。
とはいえ作ってみるとなかなか面白いので,拡張機能の開発ももっと盛り上がってもいいのにな~と思います。拙い記事ですが,その一助になれれば幸いです。
今回はフロントエンドで完結する拡張機能を作成しましたが,今回の方法を応用することでサーバー側と処理を連携させることも可能です。今後はそちらも取り組んでみたいと思います。
ではまた。
Discussion