Xeus関連の情報(PyData Osaka 2020/8/2用の記事)

Xeus関連の情報(PyData Osaka 2020/8/2用の記事)

  • 最近のJupyter Blogから一部記事を抜粋して紹介します。
  • Xeus関連の情報がメインです

Xeus is now Jupyter subproject (2020/2/4)

Xeusとは

  • Jupyter kernel protocolのC++実装
    • ipykernelの代替として使用可能
  • Xeusはカーネルではなくカーネルを作るためのライブラリ
  • いくつかのカーネルはXeusを元に作られている
    • 下記はXeusメンテナたちによるもの
    • xeus-cling
      • CERNのCling C++インタプリタベースのカーネル
    • xeus-python
    • xeus-calc
      • 電卓カーネル、Xeusの使い方を学ぶためのプロジェクト
  • Xeusプロジェクトとは関係なく作成されているカーネル
    • JuniperKernel
      • R用カーネル
    • xeus-fift
      • fiftなるTelegramによって作られたTON blockchain contractのためのプログラミング言語向けカーネル
    • SlicerJupyter
      • Kitwareによる"Slicer"プロジェクトのQtイベントループと統合されたPython向けカーネル

XeusがJupyter管轄下へと移行した理由

以下に引用

While Xeus started as a side project for QuantStack engineers,
the project now has several stakeholders who depend on it.
We think that moving the project to an open governance organization may be a better way to reflect this situation.

A visual debugger for Jupyter (2020/3/26)

  • 日本語の紹介記事
    • https://www.publickey1.jp/blog/20/jupyterlab.html

概要

  • Jupyter向けのvisual debuggerに関する紹介
  • debuggerでできることの例
    • ブレークポイントの設定(ノートブックのセルおよびソースファイル)
    • 変数一覧の表示
    • コールスタックの表示
  • リンク先のbinderから試しに実行可能

インストール方法

フロントエンドとなるプラグインとバックエンドとなるカーネルが必要

フロントエンド
$ jupyter labextension install @jupyterlab/debugger

このデバッガフロントエンドは将来のリリースにおいてはデフォルトで同梱される予定。

バックエンド
  • Jupyter Debug Protocol(次の節で説明)を実装したカーネルが必要
    • このプロトコルを実装したカーネルはいまのところxeus-pythonのみ
  • ipykernelにデバッガプロトコルを実装するのはロードマップには存在
$ conda install xeus-python -c conda-forge
  • プラットフォームによってはPyPIのwheelsも使えるがまだ実験段階のもの

  • condaで試しにinstallして試してみたところxeus-pythonではmatplotlibがうまく扱えない模様

    • https://github.com/jupyter-xeus/xeus-python/issues/224

Jupyter Debug Protocol

ControlおよびIOPub Channelに対する新しいメッセージタイプ
  • Jupyter kernelはプロセス間コミュニケーションプロトコル(https://jupyter-client.readthedocs.io/en/stable/messaging.html)に基づき動作
    • Shellチャンネルはコード実行のリクエストなどのrequest/replyを行う
    • IOPubチャンネルはkernelからclientへの片方向のチャンネル(標準出力やエラー出力で使用)
    • ControlチャンネルはShellチャンネルに似ているが別ソケットで実行され、実行キューに溜めないhigh priorityなメッセージを扱う
      • InterruptやShutdownリクエストなど
      • debuggerに送られるコマンドに対してControlチャンネルを使うことに決めた
  • protocolに2つのメッセージタイプを追加
    • debug_request/reply
      • breakpointの追加やコードに対するstep into操作などのdebuggerによって実行される特定のアクションのrequest
      • Controlチャンネルに送られる
    • debug_event
      • debugging kernelからフロントエンドに対して片方向に送られるdebug eventメッセージ
      • IOPubチャンネルを通じて送られる
Debug Adapter Protocolの拡張
  • Jupyterにおけるデザインのキーとなる原理としてプログラミング言語について不可知であるというものがある
    • Jupyter debug protocolが他のカーネル実装に対しても適用できるものであることは重要である
  • 標準的なプロトコルとしてMicrosoftの"Debug Adapter Protocol"(DAP)がある
    • JSONベースのprotocolでありVSCode下で様々な言語のバックエンドとして既に動作
  • JupyterでDAPを用いることが自然であるが、Jupyterにおける要件を満たすためには十分ではない
    • ページ再読み込みのサポートが必要
      • Jupyter kernelはデバッガの状態(breakpointや現在どこでstopしているか)を後から接続したclientのために保持しなければならない
      • フロントエンドはその状態をdebug_requestメッセージで要求できる
    • ソースファイルベースではないnotebookのcellとconsoleに対するデバッグのサポートが必要
      • ブレークポイントを追加できるコードをdebuggerに送るためのメッセージが必要
  • これらの変更はJEPとして提案されている

Xeus-python, the first Jupyter Kernel to support debugging

  • XeusはC++はJupyter kernelプロトコルのC++実装
    • それ自身がkernelではなくkernel作成を補助するlibrary
    • CやC++ APIを持つ言語に対するカーネルを作成するのに便利
      • Python、Lua、SQLなど
  • Xeus-pythonはdebugging protocolを実装する最初の実装として適切
    • プラガブルなconcurrencyモデルを持っているためControlチャンネルを別スレッドで動作させることが可能
    • 上記を繰り返し動作させるために便利なサンドボックスを持つ軽量なコードベースを持つ
      • ipykernelでdebuggingプロトコルを実装することはかなりのリファクタリングを要し早い段階でのconsensus buildingが必要となる
  • Xeus-pythonのロードマップ
    • ipykernelに対して失われているmagicを追加
    • PyPI wheelsを改善
  • 他のカーネルにおけるdebugging
    • xeusベースのkernelについては大部分の実装が共有可能であるため例えばxeus-clingなどでは早期に使用可能となる

Diving into the debugger front-end architecture

  • jupyterlabのdebugger extensionはユーザーが典型的なIDEから予想するようなUIを含む
    • サイドバーに変数エクスプローラ、ブレークポイントのリスト、ソースプレビュー、コールスタックを表示
    • コードの横に直接ブレークポイントを設定できる(cell/console)
    • 現在コードのどの部分で停止しているかを示すヴィジュアルマーカー
  • screencastで種々の機能を説明
    • 実行後に削除したセルに対するstep intoはread-only viewとして表示される
    • コンソールおよびファイルに対してもデバッガは有効
    • デバッグはノートブックレベルで実行されるため異なるノートブックに対して同時にデバッグを実行できる
    • 変数一覧はtree形式およびtable形式で閲覧可能

Future developments

  • 2020年の予定
    • 変数ビューワでmime type renderingのサポート
    • 条件付きブレークポイントのサポート
    • debugger UXの向上
    • Voilå dashboardのデバッグの有効化

A Jupyter Kernel for SQLite

概要

  • SQLite用jupyterカーネルの紹介
    • SQLiteのシンタックスをcode cellで受け付ける
    • magixコマンドでDBのオープンやクローズなどを行う
  • Xeusを用いて作成

現在の状況

  • 現在進行形で開発中だが下記を備えている
    • SQLiteインタフェースに対する完全な機能
    • 高レベル操作のためのmagic
      • DBの作成、オープン、クローズ、バックアップ
      • テーブルの存在チェック、keyの設定および解除、それらの情報の取得
  • テーブル表示
    • jupyter lab/notebookではHTMLで表示
    • コンソールではtabulateライブラリを使って表示

future

  • 直感的なデータ表示(plot, graph, chart, mapsなど)の描画
    • Vegaを使えないかを検討中(ブログの画像を参照)
  • xeus-SQLiteとSQLiteライブラリのstaticビルドをバンドルしたシングルバイナリの供給

その他

  • binderで試しに実行可能(ブログ記事にリンク有)
  • インストール方法
    • mamba install xeus-sqlite -c conda-forge
    • conda install xeus-sqlite -c conda-forge
    • mambaはcondaのc++による再実装らしい
      • 並列ダウンロードや高速な依存性解決を備えるとのこと

SlicerJupyter: a 3D Slicer kernel for interactive publications

概要

  • 3D Slicer
    • Qtを用いたC++で書かれたデスクトップアプリケーション
      • 医用画像の分析および可視化に使用
    • 3D描画にVisualization Toolkit(VTK)を用い、画像処理にInsight Toolkit(ITK)を用いている
    • pythonインタプリタが埋め込まれている
    • xeus-pythonのインタプリタを統合すればQtベースのデスクトップアプリケーションがnotebookを通じて操作できることを紹介
  • githubのSlicerJupyterに実装が存在
    • BlenderやFreeCAD、ParaViewといったpythonが埋め込まれた他のアプリケーションに対しても拡張可能
  • xeus-python integrationがJupyterのエコシステムとデスクトップアプリケーションの両方に対して必要不可欠

Powerful Medical Imaging Capabilities Available Through Jupyter

  • 3D Slicer(以下Slicer)は内蔵しているpythonインタプリタを通じてすべての機能をpyhonから操作可能
    • pythonインタプリタに対する単純なコンソールは備えているがcellベースのインタラクティブ環境は備えていない
  • xeus-pythonを統合することによってSlicerのプロセスをJupyter kernelとして使用可能
  • xeus-pythonはitkwidgetsのようなJupyter widgetsへのインタフェースとなるだけではなく、SlicerインタプリタのようなカスタムインタプリタやQtイベントループのようなGUIイベントループとも統合することが可能
    • これによりSlicer内部表現であるMedical Reality Markup Language(MRML)の表現が可能となる
    • ノートブック内でpandasやNumPy配列のようなpythonのエコシステムを通じて完全なmedical imaging APIとデータへとアクセスが可能となる

対話的操作におけるレベル

  • JupyterのWidget(sliderやボタンなど)をSlicer操作やデータ変更、処理やパラメータ可視化のために使える
  • Widgetを通じた対話的操作は異なるレベルごとに実装されている

  • Level1

    • Jupyter Widgetがアプリケーション固有のオブジェクトを自動変換の上で表示
    • 例えばSlicer markup fiducial listはフォーマット済みのテーブルとして表示され、モデルnodeは3Dオブジェクトとして描画される
  • Level2
    • Static image widgetがデスクトップアプリケーションが描画したコンテンツを描画
    • これらのwidgetは追加された標準Widgetを用いてデータや描画パラメータが変更されることによって作成される
    • 様々なデータ型や非常に大きなデータセットなどの洗練された描画をJupyterから直接可能とする
  • Level3
    • Dynamic viewer Widgetがデスクトップアプリケーションが描画した2D/3D viewをを描画
    • マウス/キーボードのイベントがデスクトップアプリケーションへと送られズームや回転を実現
      • アノテーションの付与、測定、画像分割のような3D対話操作をデスクトップアプリケーション上と同じように行える
      • これはipycanvasipyeventsパッケージを用いて実現される
  • Level4

    • 完全なGUI統合
      • ユーザーはアプリケーションウインドウを標準デスクトップWidget(スライダーやメニューなど)を含めてノートブックセルの中から見ることができる
      • これはSlicer Jupyterの中ではnoVNCとTigerVNCを用いて実現される
      • リモートサーバー上でアプリケーションが実行されているときに便利
  • 現在は大部分の実装については安定しているが、まだデザイン面での改善やパフォーマンスの改善の余地がある

    • 自動変換(display hookの複雑な実装による)
    • xeus-pythonのスレッディングモデルはメインスレッドをロックしないように改良する余地がある
    • Level3の統合におけるdynamic viewer widgetはより高いリフレッシュレート実現のためにパフォーマンス最適化の余地がある

History of Slicer and this integration

  • QtベースのデスクトップアプリケーションによってXeusカーネルのイベントループが駆動されている?

jupyterlabのwav用MIMEレンダラーを作成しました

概要

jupyterlab内でwavファイルが開けなかったのでチュートリアルを参考にMIMEレンダラーを作成しました。 正直wavファイルの場合は上記チュートリアルと全く同じ操作で作成できてしまいました。

プロジェクトの初期化

cookiecutterを使って雛形を生成します。

❯ cookiecutter https://github.com/jupyterlab/mimerender-cookiecutter-ts.git
author_name []: wrist
author_email []: stoicheia1986@gmail.com
extension_name [myextension]: jupyterlab-wav
viewer_name [My Viewer]: JupyterLab wav viewer
mimetype [application/vnd.my_organization.my_type]: audio/wav
mimetype_name [my_type]: wav
file_extension [.my_type]: .wav
Select data_format:
1 - string
2 - json
Choose from 1, 2 (1, 2) [1]: 1

Extensionのビルドとインストール

Extensionの作成にはyarnのバージョンが固定されたjlpmというjupyterlab付属のツールを用いて行います。 依存パッケージをインストールし、Extensionをビルドし、jupyterlabのextensionとしてインストールするためには下記を実行します。

jlpm install
jlpm run build
jupyter labextension install . --no-build

ここで上記をローカル環境で試したところ、jupyterlabのバージョンが1.2.1の場合は現在のJupyterlabとはバージョンの互換性がないというエラーがでてしまいました(既に2.0.0以上を想定している模様です)。

このためjupyterlabを動作させるために使用しているdocker imageであるwrist/jupyterlab-customを更新したのですが、 jupyterlab-vimは2.x系に対応してなかったので代わりに https://github.com/jwkvam/jupyterlab-vim/pull/115 などを参照し、 @axlair/jupyterlab_vimをインストールしています。 なお、ローカルでdocker imageをビルドする際に当初jupyter lab buildを実行すると このissueと同じようにensure-max-old-space実行時にエラーが出てbuildできなくなりましたが、 osx上でDockerが使用するメモリを4096MBにしたところエラーが生じなくなりました。

コードの監視

jlpm run watchでextensionのコードに変更があるとすぐにrecompileしてくれるようになります。 またjupyterlabをjupyter lab --watchとして立ち上げるとその変更を監視しアプリケーションに取り込んでくれるようになります。 しかし手元ではwebpackがファイル監視に使用しているchokdairを見つけられないというエラーで動作しないため一旦諦めました。 なお、上記watchを施さなくてもjupyterlab自体をreloadするとコードに変更があった場合はbuildを促されるので大きな問題は生じませんでした。

※7/8追記: この監視がうまく行かない件ですがこのissueによればv2.1.3のバグとのことでv2.1.4にjupyterlabのバージョンを上げたら解決しました。

コードの構造

自動生成されたコードのsrc/index.tsを見ると主に3つのデータ構造を含んでいます。

  • OutputWidgetクラス
    • 指定したMIMEタイプのデータを受け取りHTML DOMノードの中にどのように描画するのかを扱うクラス
    • extensionのほとんどのロジックを含む
  • rendererFactoryオブジェクト
    • OutputWidgetクラスのインスタンスをどのようにアプリケーション内で生成するのかを扱うクラス
  • extensionオブジェクト
    • extensionのメインのエントリーポイントとなる部分
    • jupyterlabがextensionをロードする際に必要となるメタデータを書く

コードの編集

下記編集を実施します。

  • OutputWidgetのリネーム
    • src/index.ts内のOutputWidgetWavWidgetへとリネームします(2箇所)
  • extensionオブジェクトのfileTypesmodelNameにbase64エンコードを指定
    • デフォルトだとプレーンテキストとして読もうとするのでbase64エンコードされたものとして読むための指定を追記
    • fileTypefileFormat: 'base64'documentWidgetFactoryOptionsmodelName: 'base64'を追加する
  • レンダー方法の指定
    • WavWidgetクラスを編集
      • コンストラクタにaudioタグを追加するためのコードを追加
      • renderModelメソッド内でaudioタグのsrcを指定

大した量ではないためコード全体を下記に記します。

import { IRenderMime } from '@jupyterlab/rendermime-interfaces';



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

/**
 * The default mime type for the extension.
 */
const MIME_TYPE = 'audio/wav';

/**
 * The class name added to the extension.
 */
const CLASS_NAME = 'mimerenderer-wav';

/**
 * A widget for rendering wav.
 */
export class WavWidget extends Widget implements IRenderMime.IRenderer {
  /**
   * Construct a new output widget.
   */
  constructor(options: IRenderMime.IRendererOptions) {
    super();
    this._mimeType = options.mimeType;
    this.addClass(CLASS_NAME);
    /* 追加 */
    this._audio = document.createElement('audio');
    this._audio.setAttribute('controls', '');
    this.node.appendChild(this._audio);
  }

  /**
   * Render wav into this widget's node.
   */
  renderModel(model: IRenderMime.IMimeModel): Promise<void> {

    let data = model.data[this._mimeType] as string;
    /* 元コードを削除し下記を追加 */
    this._audio.src = `data:${MIME_TYPE};base64,${data}`

    return Promise.resolve();
  }

  private _audio: HTMLAudioElement;
  private _mimeType: string;
}

/**
 * A mime renderer factory for wav data.
 */
export const rendererFactory: IRenderMime.IRendererFactory = {
  safe: true,
  mimeTypes: [MIME_TYPE],
  createRenderer: options => new WavWidget(options)
};

/**
 * Extension definition.
 */
const extension: IRenderMime.IExtension = {
  id: 'jupyterlab-wav:plugin',
  rendererFactory,
  rank: 0,
  dataType: 'string',
  fileTypes: [
    {
      name: 'wav',
      fileFormat: 'base64',  // 追加
      mimeTypes: [MIME_TYPE],
      extensions: ['.wav']
    }
  ],
  documentWidgetFactoryOptions: {
    name: 'JupyterLab wav viewer',
    primaryFileType: 'wav',
    modelName: 'base64',  // 追加
    fileTypes: ['wav'],
    defaultFor: ['wav']
  }
};

export default extension;

上記コードを再度ビルドしてjupyterlabを再読み込みすれば完了です。

実際の表示の例

左のファイルブラウザからwavファイルをダブルクリックすると下記のようにaudio要素が表示されます。

audio要素

ちなみにwavファイルを右クリックして表示される「Open in New Browser tab」を実行するとこのExtensionがなくてもブラウザの別タブで再生は元々可能です。

npmjsへの投稿

Extension Developer GUidShipping Packagesの項を読むと extensionは単一のJavascriptパッケージであり、npmjs.orgから配布できることや、 jupyterlab-extensionというキーワードがpackage.jsonに含まれている場合JupyterLabのextension managerが見つけ出すことができるとの記載があります。 このキーワードはcookiecutterで自動生成された場合既に含まれているため、自ら追加する必要はありません。

よってnpmjs.orgへと登録することを考えます。 まずコマンドラインからnpmjsにログインしておきます。

$ npm adduser  # ユーザをnpmjs上に作成していない場合
$ npm login

次にpackage.jsonを編集し、nameの値をjupyterlab-wavから@wrist/jupyterlab-wavというように@username/extension-nameへと変えます。 更に必要に応じてhomepagelicenserepositoryなどのフィールドを追加します。

最後に、README.mdを編集してjupyter labextension installの部分を@wrist/jupyterlab-wavのように変更します。

上記を終えたら下記でnpmjsへとアップロードして終了です。

$ npm publish --access=public

無事にhttps://www.npmjs.com/package/@wrist/jupyterlab-wavへとアップロードされていれば、以後

$ jupyter labextension install @wrist/jupyterlab-wav

と打つことでインストールできるようになります。 実際に新規にコンテナを生成し直して上記を試したところ使えるようになりました。

今後

  • 他のMIMEタイプにも対応させる
    • 人によってはmp3も同様に開きたいこともあるかもしれませんが、MIME_TYPEの部分が配列になっていることから複数拡張子に対して同じような実装を使いませせるような気がしています
  • Web Audio APIを用いて波形描画やスペクトログラムを描画する

nbmediasplitというipynbファイルから画像・音声を抽出するためのスクリプトをPyPIで公開しました

nbmediasplitというipynbファイルから画像・音声を抽出するためのスクリプトをPyPIで公開しました

nbmediasplitというipynbファイルにbase64エンコードされて埋め込まれた画像・音声を抽出するためのスクリプトをPyPIで公開しました。

この記事では使い方と使用しているツールについて簡単に記述します。

インストール・使い方

  • pip install nbmediasplitを実行
    • nbmediasplitコマンドが使用可能となる
In [1]:
!pip install nbmediasplit
Collecting nbmediasplit
  Downloading https://files.pythonhosted.org/packages/fd/e2/02c36a9c5322bd24b43b3d2abe7b5ea80827e793a6b2f35b9b5460a7b4f0/nbmediasplit-0.2.0-py3-none-any.whl
Collecting click<8.0,>=7.1
  Downloading https://files.pythonhosted.org/packages/d2/3d/fa76db83bf75c4f8d338c2fd15c8d33fdd7ad23a9b5e57eb6c5de26b430e/click-7.1.2-py2.py3-none-any.whl (82kB)
     |████████████████████████████████| 92kB 798kB/s eta 0:00:01
Collecting beautifulsoup4<5.0,>=4.9
  Downloading https://files.pythonhosted.org/packages/66/25/ff030e2437265616a1e9b25ccc864e0371a0bc3adb7c5a404fd661c6f4f6/beautifulsoup4-4.9.1-py3-none-any.whl (115kB)
     |████████████████████████████████| 122kB 5.4MB/s eta 0:00:01
Requirement already satisfied: lxml<5.0,>=4.5 in /opt/conda/lib/python3.7/site-packages (from nbmediasplit) (4.5.0)
Requirement already satisfied: soupsieve>1.2 in /opt/conda/lib/python3.7/site-packages (from beautifulsoup4<5.0,>=4.9->nbmediasplit) (1.9.4)
ERROR: distributed 2.5.2 has requirement dask>=2.3, but you'll have dask 2.2.0 which is incompatible.
Installing collected packages: click, beautifulsoup4, nbmediasplit
  Found existing installation: Click 7.0
    Uninstalling Click-7.0:
      Successfully uninstalled Click-7.0
  Found existing installation: beautifulsoup4 4.8.1
    Uninstalling beautifulsoup4-4.8.1:
      Successfully uninstalled beautifulsoup4-4.8.1
Successfully installed beautifulsoup4-4.9.1 click-7.1.2 nbmediasplit-0.2.0
In [2]:
!nbmediasplit --help
Usage: nbmediasplit [OPTIONS] IPYNB_FILE

  extract base64 encoded image and pcm and save them into specified
  directories.

Options:
  -i, --imgdir TEXT    directory to store image
  -w, --wavdir TEXT    directory to store audio
  -o, --output TEXT    output ipynb file path
  -e, --encoding TEXT  input ipynb encoding  [default: utf-8]
  -d, --debug          use debug mode
  --img-prefix TEXT    path prefix for src attribute of img tag
  --wav-prefix TEXT    path prefix for src attribute of source tag under audio
                       tag

  --help               Show this message and exit.

ipynbファイルから画像を抽出

In [3]:
!nbmediasplit work/test.ipynb -i work/img
In [4]:
!ls work/img
0.png  1.png  2.png  3.png  4.png  5.png

現在のスクリプトではコードセルのoutputに埋め込まれた画像だけでなく、markdownセルに埋め込まれたattachmentも同時に抽出されます。

In [5]:
from IPython.display import Image
Image("work/img/0.png")
Out[5]:

ipynbファイルから音声を抽出

In [6]:
!nbmediasplit work/test.ipynb -w work/wav
In [7]:
!ls work/wav
0.wav  1.wav  2.wav

上記はコードセルのoutputに埋め込まれた音声を抽出します。0.wavは1kHz、FS48kHzの正弦波です。振幅が大きい(0dBFS)のでヘッドホンで再生する場合はご注意ください。 なお-iオプションと-wを同時に付けることで画像と音声を同時に抽出することもできます。

In [10]:
from IPython.display import Audio
Audio("work/wav/0.wav")
Out[10]:

ipynbファイルから画像・音声を抽出し、ipynbファイルのbase64エンコード部分を実ファイルパスに置換したipynbファイルを新規に作成

-oオプションを付けると、入力したipynbにおいてbase64エンコードされて埋め込まれた画像・音声を実ファイルへのパスに置換したipynbを新規に作成します。

In [27]:
!nbmediasplit work/test.ipynb -i work/img -w work/wav -o work/test_converted.ipynb
In [28]:
!grep img work/test_converted.ipynb
                            "<img src=\"work/img/0.png\" />"
                            "<img src=\"work/img/1.png\" />"
                        "<img src=\"work/img/2.png\" />"
                            "<img src=\"work/img/3.png\" />"
                            "<img src=\"work/img/4.png\" />"
                            "<img src=\"work/img/5.png\" />"
In [29]:
!grep audio work/test_converted.ipynb
                "Unless you trust the notebook converted by nbmediasplit, you can't load audio source in Jupyter.\n",
                        "text/html": "<audio controls preload=\"none\"><source src=\"work/wav/0.wav\" type=\"audio/wav\" /></audio>",
                "# single audio tag\n",
                        "text/html": "<audio controls preload=\"none\"><source src=\"work/wav/1.wav\" type=\"audio/wav\" /></audio>",
                        "text/html": "<audio controls preload=\"none\"><source src=\"work/wav/2.wav\" type=\"audio/wav\" /></audio>",
                "# multiple audio tag\n",

更に、--img-prefix--wav-prefixオプションを指定すると出力ipynb中のパスを指定したパスへと変更することができます。 これは実際にファイルを出力させる場所と実際に参照させたいパスが違うケースで有効です。 下記の場合だとwork以下に出力されたipynbではwork/imgなどではなくimgを参照しなければ相対パスが合いませんが、これらのオプションでこの部分を修正することができます。

In [30]:
!nbmediasplit work/test.ipynb -i work/img -w work/wav -o work/test_converted.ipynb --img-prefix=img --wav-prefix=wav
In [31]:
!grep img work/test_converted.ipynb
                            "<img src=\"img/0.png\" />"
                            "<img src=\"img/1.png\" />"
                        "<img src=\"img/2.png\" />"
                            "<img src=\"img/3.png\" />"
                            "<img src=\"img/4.png\" />"
                            "<img src=\"img/5.png\" />"
In [32]:
!grep audio work/test_converted.ipynb
                "Unless you trust the notebook converted by nbmediasplit, you can't load audio source in Jupyter.\n",
                        "text/html": "<audio controls preload=\"none\"><source src=\"wav/0.wav\" type=\"audio/wav\" /></audio>",
                "# single audio tag\n",
                        "text/html": "<audio controls preload=\"none\"><source src=\"wav/1.wav\" type=\"audio/wav\" /></audio>",
                        "text/html": "<audio controls preload=\"none\"><source src=\"wav/2.wav\" type=\"audio/wav\" /></audio>",
                "# multiple audio tag\n",

埋め込まれているパス名が変わっていることが分かると思います。

なお、出力ファイルである変換後のipynbにパスとして埋め込まれた画像ファイルを表示したりオーディオファイルを再生する場合はそのnotebookをtrustedなものとしなければなりません。このためには下図のようにコマンドパレットからTrust Notebookを実行する必要があります。

In [33]:
Image("work/img/2.png")
Out[33]:

開発時に使用した各種ツール類

このスクリプトを作成する際に下記ライブラリを使用しています。

  • poetry
    • 依存関係の処理
    • パッケージング、公開
  • click
    • 引数処理
  • pytest
    • テスト
  • invoke
    • コマンドラインタスク実行
  • autopep8
    • スクリプトの整形
    • 当初blackを試したが、ネストされたエスケープ付きのダブルクォーテーションのエスケープ文字を除去するなど適さない動作があったため不採用
  • beautifulsoup4lxml
    • HTMLの加工処理
  • soundfile
    • 音声処理

また、自動テストのためにgithub actionsを使用しています。 ここではこれらの項目の一部についてそれぞれ簡単に触れたいと思います。

poetry

poetryでは依存ライブラリの処理やパッケージング、PyPIへの公開などを行いました。

  • poetry init
    • pyproject.tomlを生成
  • poetry add library_name
    • 依存関係を追加
    • -Dを付けると開発用ライブラリとして追加
  • poetry install
    • 依存ライブラリをインストール
  • poetry run ...
    • 仮想環境内でコマンドを実行
      • poetry run python ... # スクリプトの実行
      • poetry run pytest # テストの実行
      • poetry run nbmediasplit # 追加したコマンド(後述)の実行
  • poetry build
    • 公開用に諸々のビルドを実行してくれる
  • poetry publish
    • PyPIに公開
    • -r testpypiなどと指定すると公開先をTestPyPIなどに変更可能
プロジェクトのファイル構造

ファイル構造は概ね下記のようになっています。

├── dist
├── poetry.lock
├── pyproject.toml
├── readme.md
├── src
│   ├── nbmediasplit
│   │   ├── __init__.py
│   │   ├── nbmediasplit.py
│   └── nbmediasplit.egg-info
│       ├── PKG-INFO
│       ├── SOURCES.txt
│       ├── dependency_links.txt
│       ├── entry_points.txt
│       ├── requires.txt
│       └── top_level.txt
├── tasks.py
├── tests
    ├── __init__.py
    ├── input
    └── test_nbmediasplit.py
PyPIでの公開時にコマンドとして実行できるスクリプトの指定

下記記述をpyproject.tomlに行うことで、nbmediasplitコマンドがPyPIでの公開時に使用できるようになります。

[tool.poetry.scripts]
nbmediasplit = "nbmediasplit.nbmediasplit:main"

これはnbmediasplitライブラリのnbmediasplit.pyファイルにおけるmain関数を実行するという意味です。

PyPIページの説明をreadme.mdから読み込む設定
[tool.poetry]
readme = "readme.md"

と指定しておくとPyPI側でも勝手にreadme.mdを使用してくれるようになります。

click

スクリプトに対して引数を指定するために使いました。clickでは容易にサブコマンドなどを作ることができると聞きますが、ここでは単なる引数の指定だけであるため、argparseでできることとほぼ同じことを行っています。argparseではコードとして引数関連の処理を書きますが、clickではデコレータとして処理が下記のように書けるので記述が散らばらず、かつ煩雑にならずに済みます。

@click.command(help='extract base64 encoded image and pcm and save them into specified directories.')
@click.argument('ipynb_file', type=click.Path(exists=True))
@click.option('-i', '--imgdir', 'img_out_dir', type=str, help='directory to store image', required=False)
@click.option('-w', '--wavdir', 'wav_out_dir', type=str, help='directory to store audio', required=False)
@click.option('-o', '--output', 'new_ipynb_filename', type=str, help='output ipynb file path', required=False)
@click.option('-e', '--encoding', 'encoding', type=str, help='input ipynb encoding',
              required=False, default="utf-8", show_default=True)
@click.option('-d', '--debug', 'use_debug', is_flag=True, help='use debug mode', required=False)
@click.option('--img-prefix', 'img_prefix', type=str, help='path prefix for src attribute of img tag', required=False)
@click.option('--wav-prefix', 'wav_prefix', type=str,
              help='path prefix for src attribute of source tag under audio tag', required=False)
def main(ipynb_file, img_out_dir, wav_out_dir, new_ipynb_filename, img_prefix, wav_prefix, encoding, use_debug):
    ...

@click.commandでclickが扱う対象とする関数を指定し、@click.argumentはオプション扱いではない引数(引数として指定するための-x--xxxが不要な引数)、@click.optionはオプション扱いとなる引数を指定しています。

invoke

当初はコマンドラインタスクをMakefileで記述していましたが、Windows環境でもテストする場合にMakefileだと(nmakeを使えば使えなくはないですが)面倒なので、共通して使うことができるタスク処理環境としてinvokeを使ってみました。 invokeではtasks.pyの内部に@taskデコレータが付与された関数を定義しておくとinv 関数名としてタスクを実行できるようになります。 下記にtasks.pyの一部を抜粋します。

@task
def clean(c):
    """clean generated files"""
    ...

@task(pre=[clean])
def pytest(c):
    """execute pytest"""
    c.run("poetry run pytest")

@task(pre=[clean])
def cuitest(c, debug=False):
    """execute cuitest"""
    ...

@taskに対してprepostに依存タスクの一覧をlistとして与えることができます。 また各タスクの引数、たとえばcuitestdebuginv cuitest --debug=Trueinv cuitest -d=Trueとしてinvコマンド使用時に指定することができます。

GitHub Actionsでのpoetryを用いたpytestの実行

複数バージョンのpythonや複数プラットフォーム上でpytestを実行する場合はtoxを使うこともできますが、 今回はGitHub Actionsを使ってみたかったので下記のようなworkflowを定義したyamlファイルを.github/workflows/pytest.yamlとして配置しています。 poetry installで依存ライブラリをインストールし、poetry pytestを複数バージョン、複数プラットフォーム上で実行しています。

name: pytest

on: [push]

jobs:
  build:

    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        python-version: [3.5, 3.6, 3.7, 3.8]
        os: [ubuntu-latest, macos-latest, windows-latest]

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install poetry
        poetry install
    - name: Test with pytest
      run: |
        poetry run pytest

今回のスクリプトを作成した動機

ipynbでブログを書く際にbase64エンコードされたデータがそのままになっている(分離されていない)とファイルサイズが非常に大きくなることがあったため、過去に分離用の単一のスクリプトを書いて使用していたのですが、これをちゃんとした形(各種引数の付与、テストの付与など)として公開したかったためです。 poetryでは新規にプロジェクトを始める場合でなくとも、poetry initpyproject.tomlファイルを生成することができるため、比較的容易に元スクリプトを加工して形を整えていくことができました。

今後

現在は画像ファイルの抽出時にattachmentとコードセル出力を等価なものとして扱っていますがこれを分離できればと思っています。また、現在は単なる連番としてファイルが出力されてしまいますが、何らかの形でコードセルの情報(execution countなど)と紐付けた名称にした方が良いとも感じています。 余力があればjupyter labのextensionとして実行できるようにもしてみたいと思いました。

またバグを見つけましたら報告いただけると幸いです。