Pyodideを使ってnwdiag(blockdiag)をクライアントサイドで動かす

# Pyodide
ブラウザ上でPythonを動かすという試みには[PyPy.js](https://github.com/pypyjs/pypyjs "PyPy.js")や[Brython](https://github.com/brython-dev/brython "Brython")といったものが以前からある。[Pyodide](https://github.com/pyodide/pyodide "Pyodide")は後発ではあるもののpipの簡易実装であるmicropipが用意されていて、[PyPI](https://pypi.org/ "PyPI")上のパッケージを(ものによっては)直接インストールできるところが画期的。

# nwdiag
[nwdiag](https://github.com/blockdiag/nwdiag "nwdiag")(や[blockdiag](https://github.com/blockdiag/blockdiag "blockdiag"))は、[PlantUML](https://github.com/plantuml/plantuml "PlantUML")や[Graphviz](https://gitlab.com/graphviz/graphviz "Graphviz")などと同じく、**テキストで定義して図を描くためのツール**。この手のツールでは[Mermaid](https://github.com/mermaid-js/mermaid "Mermaid")の記法が最近GitHubでサポートされて話題になった。[nwdiag](https://github.com/blockdiag/nwdiag "nwdiag")はそんな**テキストで定義して図を描くツール**の中でも特にネットワーク構成図に特化したもの。

# REPLで試す
[Pyodideのサイト](https://pyodide.org/ "Pyodideのサイト")にはデモ用の[REPL](https://pyodide.org/en/stable/console.html "REPL")が用意されており、[WASM](https://developer.mozilla.org/ja/docs/WebAssembly "WebAssembly")に対応したブラウザさえあれば手軽に試すことができる。今回はそれを使って[nwdiag](https://github.com/blockdiag/nwdiag "nwdiag")を動かしてみる。

### パッケージのインストール
パッケージのインストールはpipで指定するのと同じ名前をmicropipに指定するだけ。
```
Welcome to the Pyodide terminal emulator 🐍
Python 3.9.5 (default, Feb 22 2022 14:12:02) on WebAssembly VM
Type "help", "copyright", "credits" or "license" for more information.
>>> import micropip
>>> await micropip.install('nwdiag')
>>>
```
あっけなく完了。
### REPLから実行する準備
もともとnwdiagはCLI上から、
```
$ nwdiag -Tsvg simple.diag
```
と実行するように設計されているため、REPL上で実行するためにはnwdiagコマンドが内部的に呼び出している処理を調べて真似る必要がある。
[コマンド周りのソース](https://github.com/blockdiag/nwdiag/blob/master/src/nwdiag/command.py "コマンド周りのソース")を見てみると、command.pyのmain関数をCLIで渡す引数と同じものを配列で渡して呼び出せば良さそうだ。
command.pyをimportする。
```
>>> import nwdiag.command
>>>
```
とりあえず、何事もなくimportできた。
### REPLから実行
早速、何かdiagファイルを用意して変換を試したいが、REPL環境に取り込むのが手間なので、とりあえずパッケージに含まれているテスト用のファイルで試すことにする。
Pyodideには[emscripten](https://github.com/emscripten-core/emscripten "emscripten")によって用意された[仮想的なファイルシステム](https://emscripten.org/docs/api_reference/Filesystem-API.html "仮想的なファイルシステム")が備わっており、そのファイルシステムの/lib/python3.9/site-packages/nwdiag/tests/diagrams以下に、GitHubの[この辺りのファイル](https://github.com/blockdiag/nwdiag/tree/master/src/nwdiag/tests/diagrams "この辺りのファイル")が存在している。どれでもいいが、node_belongs_to_multiple_networks.diagで試してみる。
```
>>> nwdiag.command.main(["-Tsvg","/lib/python3.9/site-packages/nwdiag/tests/diagrams/node_belongs_to_multiple_networks.diag"])
ERROR:
unknown format: SVG

-1
>>> 
```
動かない。。。
### 不具合箇所の特定
エラーメッセージを手がかりにnwdiag(と描画を担っているblockdiag)のソースを追ってみると、依存パッケージが足りていないようだ。
インストール済みのパッケージはmicropip.list()で確認できる。
```
>>> micropip.list()
Name          | Version | Source
------------- | ------- | -------
webcolors     | 1.11.1  | pypi
funcparserlib | 1.0.0a0 | pypi
blockdiag     | 3.0.0   | pypi
nwdiag        | 3.0.0   | pypi
setuptools    | 60.3.1  | pyodide
pillow        | 9.0.0   | pyodide
distutils     | 1.0     | pyodide
micropip      | 0.1     | pyodide
pyparsing     | 3.0.6   | pyodide
packaging     | 21.3    | pyodide
>>>
```
足りているように見える。。。
さらにソースを読み進んで依存パッケージの有無をどのように判定しているのかまで追ってみる。
この辺りだ。
https://github.com/blockdiag/blockdiag/blob/master/src/blockdiag/imagedraw/__init__.py
```python:__init__.py
def init_imagedrawers(debug=False):
    for drawer in pkg_resources.iter_entry_points('blockdiag_imagedrawers'):
        try:
            module = drawer.load()
            if hasattr(module, 'setup'):
                module.setup(module)
        except Exception as exc:
            if debug:
                warning('Failed to load %s: %r' % (drawer.module_name, exc))
```
pkg_resourcesが管理しているパッケージ情報がどうなっているのか確認してみる必要がありそうだ。
```
>>> import pkg_resources
>>> pkg_resources.working_set.by_key
{'tzdata': tzdata 2021.5 (/lib/python3.9), 'setuptools': setuptools 60.3.1 (/lib/python3.9/site-packages), 'packaging': packaging 
21.3 (/lib/python3.9/site-packages), 'unknown': UNKNOWN 9.0.0 (/lib/python3.9/site-packages), 'pyparsing': pyparsing 3.0.6 (/lib/p
ython3.9/site-packages), 'blockdiag': blockdiag 3.0.0 (/lib/python3.9/site-packages), 'nwdiag': nwdiag 3.0.0 (/lib/python3.9/site-
packages), 'webcolors': webcolors 1.11.1 (/lib/python3.9/site-packages), 'funcparserlib': funcparserlib 1.0.0a0 (/lib/python3.9/si
te-packages), 'micropip': micropip 0.1 (/lib/python3.9/site-packages)}
>>>
```
'unknown': UNKNOWN 9.0.0って何だ？
micropipの出力と比較してみると9.0.0というバージョン番号からみて[pillow](https://pypi.org/project/Pillow/ "pillow")で間違いなさそうだ。(画像関係のパッケージだし)
'pillow': pillow 9.0.0のようになるべきところ、何かの不具合でunknownとなってしまっているのだろう。
### 不具合の回避
パッケージ情報を修正してみる。
```
>>> for dist in pkg_resources.distributions_from_metadata('/lib/python3.9/site-packages/Pillow-9.0.0.dist-info'):
...   pkg_resources.working_set.by_key['pillow'] = dist
... 
>>> pkg_resources.working_set.by_key
{'tzdata': tzdata 2021.5 (/lib/python3.9), 'setuptools': setuptools 60.3.1 (/lib/python3.9/site-packages), 'packaging': packaging 
21.3 (/lib/python3.9/site-packages), 'unknown': UNKNOWN 9.0.0 (/lib/python3.9/site-packages), 'pyparsing': pyparsing 3.0.6 (/lib/p
ython3.9/site-packages), 'blockdiag': blockdiag 3.0.0 (/lib/python3.9/site-packages), 'nwdiag': nwdiag 3.0.0 (/lib/python3.9/site-
packages), 'webcolors': webcolors 1.11.1 (/lib/python3.9/site-packages), 'funcparserlib': funcparserlib 1.0.0a0 (/lib/python3.9/si
te-packages), 'micropip': micropip 0.1 (/lib/python3.9/site-packages), 'pillow': Pillow 9.0.0 (/lib/python3.9/site-packages)}
>>>
```
'unknown'というキーの存在はそのままだが、無事末尾に'pillow'が追加されている。
### 再チャレンジ
再度、node_belongs_to_multiple_networks.diagの変換を試みる。
```
>>> nwdiag.command.main(["-Tsvg","/lib/python3.9/site-packages/nwdiag/tests/diagrams/node_belongs_to_multiple_networks.diag"])
0
>>>
```
無事に変換できたようだ。
変換元のdiagファイルと同じディレクトリ配下に拡張子svgのファイルが生成されているはずなので読み出してみる。
```
>>> svgfile = open("/lib/python3.9/site-packages/nwdiag/tests/diagrams/node_belongs_to_multiple_networks.svg","r")
>>> svg = svgfile.read()
>>> print(svg)
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg viewBox="0 0 304 444" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:x
link="http://www.w3.org/1999/xlink">
  <defs id="defs_block">
    <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
      <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" />
    </filter>
  </defs>
  <title>blockdiag</title>
  <desc>{
  network {
    A;
  }
  network {
    A;
  }
  network {
    A;
  }
}
</desc>
  <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="104" 
x="155" y="162" />
  <path d="M 131 103 L 283 103 A2,4 0 0 1 283 111 L 131 111 A2,4 0 0 1 131 103" fill="rgb(0,0,0)" style="filter:url(#filter_blur)"
 />
  <path d="M 131 247 L 283 247 A2,4 0 0 1 283 255 L 131 255 A2,4 0 0 1 131 247" fill="rgb(0,0,0)" style="filter:url(#filter_blur)"
 />
  <path d="M 131 391 L 283 391 A2,4 0 0 1 283 399 L 131 399 A2,4 0 0 1 131 391" fill="rgb(0,0,0)" style="filter:url(#filter_blur)"
 />
  <path d="M 128 100 L 280 100 A2,4 0 0 1 280 108 L 128 108 A2,4 0 0 1 128 100" fill="rgb(185,203,228)" stroke="rgb(0,0,0)" />
  <path d="M 280 108 A2,4 0 0 1 280 100" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 128 100 L 280 100" fill="none" stroke="none" />
  <path d="M 204 35 L 204 100" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 128 244 L 280 244 A2,4 0 0 1 280 252 L 128 252 A2,4 0 0 1 128 244" fill="rgb(185,203,228)" stroke="rgb(0,0,0)" />
  <path d="M 280 252 A2,4 0 0 1 280 244" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 128 244 L 280 244" fill="none" stroke="none" />
  <path d="M 128 388 L 280 388 A2,4 0 0 1 280 396 L 128 396 A2,4 0 0 1 128 388" fill="rgb(185,203,228)" stroke="rgb(0,0,0)" />
  <path d="M 280 396 A2,4 0 0 1 280 388" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 128 388 L 280 388" fill="none" stroke="none" />
  <path d="M 204 108 L 204 156" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 212 196 L 212 244" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 196 196 L 196 240" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 196.0 240.0 A8.0,8.0 0 0 1 196.0 256.0" fill="none" stroke="rgb(0,0,0)" />
  <path d="M 196 256 L 196 388" fill="none" stroke="rgb(0,0,0)" />
  <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="104" x="152" y="156" />
  <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" te
xtLength="6" x="204.0" y="182">A</text>
</svg>
>>>
```