vscode で streamlit アプリを統合デバッグする方法

題名の結論だけ知りたい方は、[解決法の節](#解決法)をご覧ください。


# Streamlit とは

一次情報 https://www.streamlit.io/ を見て頂くと、Streamlit がどのようなもので、何が便利かがよく分かります。と書いて終わってしまうと私の出番がないので、本節の残りはご興味があればどうぞ:sweat:

私はこれまで、データの集計をする際に Pandas, Jupyter, Flask などをよく使っていたのですが、つい先日 [Streamlit](https://www.streamlit.io/) を知って触れてみたところ、とても取り扱いが軽快で気に入りました。

例えば、ちょっとRDBやREST風APIなど複数ソースから収集したデータを加工してプレゼンテーションしたい。アウトプットにはBootstrapとJQueryをほんのり当ててリッチ風にプレゼンテーションしたい。なんていう事があると思います。

これをFlaskで作ろうとすると、環境としてのDockerfileやPipfile、コンテンツとしてのテンプレート(Jinja TemplateのようなHTML)、Assets類（CSSやjsや画像）など、直接のETL行為ではない、言うなればボイラープレートの作成行為があります。

Streamlitは、前述の意味のボイラープレートが非常に少なく、直接のETL行為・・・つまり[Select](https://docs.sqlalchemy.org/en/13/core/tutorial.html#selecting)したり[GET](https://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/#id2)したデータソースを [DataFrame に飲ませて](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_json.html)フィルタやマージした後、[Plotly のチャートに飲み込ませる](https://plotly.com/python/pandas-backend/#series-and-dataframe-functions-hist-and-boxplot)。といった直接のETL処理に注力しやすいフレームワークです。

処理の[メモ化](https://flask-caching.readthedocs.io/en/latest/#memoization)やリッチなプレゼンテーションなどは、streamlitがワンライナー的なシンプルさで提供します。

つまり・・・こうして

```python:app.py
import streamlit as st
import pandas as pd

@st.cache
def very_large_data_read(file_name):
    return pd.read_csv(file_name)         # 例えば10分かかるデータ取得処理

st.write("""
# My first app
Hello *world!*
""")                                      # Markdownなどでテキストコンテンツ出力
df = very_large_data_read("my_data.csv")  # メモ化されたデータ取得
st.line_chart(df)　　　　　　　　　　　     # ワンライナーなチャート描画
```

こうすると、

```bash
$ pipenv install streamlit
$ pipenv run streamlit run app.py
  Local URL: http://localhost:8501
  Network URL: http://172.xxx.xxx.xxx:8501
```

こうなる。[^1]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/d5681b3f-fdda-8e53-08f6-fa0720fa3af9.png)

[^1]: https://www.streamlit.io/ より引用

# VSCode を用いたStreamlitアプリの統合デバッグ法

・・・ということで、Streamlit を使いたくなってきたところで、VSCodeを用いてStreamlitアプリを`統合デバッグ`する方法を紹介します。`統合デバッグ`とは、VSCodeの統合ターミナル内で、(1)ブレークポイントを張ってステップ実行する、(2)ウォッチ式を見る、(3)デバッグコンソールを利用する。といった操作を指します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/22fa50b3-26ee-86c8-271a-2e90a7b43ea5.png)

利用環境は以下です。

```text:主要環境バージョン
OS: Windows 10
VSCode: 1.51.1
Python extension for Visual Studio Code: v2020.11.371526539
Python: 3.8.6
debugpy: 1.2.0
```

### 困りごと

Streamlit のドキュメントには、よくあるPythonプログラムの起動法である `python -m ${MODULE_NAME}` ではなく、`streamlit run ${CODE_FILE_NAME}` のような[起動法が紹介](https://awesome-streamlit.readthedocs.io/en/latest/vscode.html#running-your-streamlit-app)されています。つまり、VSCode の `Integrated Debugging` が効かない起動法でStreamlitの事始めをされる方が多いと思います。

しかし、すぐに`統合デバッグ`をしたくなりますよね。その方法が、[Integrated Debugging](https://awesome-streamlit.readthedocs.io/en/latest/vscode.html#integrated-debugging) に紹介されているのですが、記述が古くこの通りでは動作しません。また、[開発が停止した ptvsd](https://github.com/microsoft/ptvsd#-%E1%B4%98%E1%B4%9B%E1%B4%A0s%E1%B4%85-%C9%AAs-%E1%B4%85%E1%B4%87%E1%B4%98%CA%80%E1%B4%87%E1%B4%84%E1%B4%80%E1%B4%9B%E1%B4%87%E1%B4%85-) の代わりとなる [debugpy](https://github.com/microsoft/debugpy/) を使う方法も動作しません。つまり、[Manual Debugging](https://awesome-streamlit.readthedocs.io/en/latest/vscode.html#manual-debugging) をする必要があるのですが・・・楽じゃないのでどうしても`統合デバッグ`をできるようにしたいのが人情です。

### 解決法

別の方法として、`${workspaceFolder}/.vscode/launch.json` を以下のようにすることで、`統合デバッグ`ができるようになります。理屈は、`python -m streamlit.cli run app.py --server.port 5678` コマンドで起動したセッションを、VSCodeでデバッグ対象とする考え方になります。

```python:${workspaceFolder}/app.py
import streamlit as st
st.title("Hello World.")
```

```json:${workspaceFolder}/.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "debug",
            "type": "python",
            "request": "launch",
            "module": "streamlit",                 // appの代わりにコレ
            "console": "integratedTerminal",
            "env": {
                "PYTHONPATH": "${workspaceFolder}",
            },
            "args": [                              // オプションはコレ
                "run",
                "${file}",
                "--server.port",
                "5678"
            ]
        }
    ]
}
```

```bash:理屈を確認
(venv)$ python -m streamlit.cli run app.py --server.port 5678

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:5678
  Network URL: http://172.xx.xxx.xxx:5678
```

### オマケ

少し横道にそれますが、対照例としてごく一般的なPythonコードをデバッグする場合の `${workspaceFolder}/.vscode/launch.json` は以下になると思います。理屈は、`python -m app source dest` コマンドで起動したセッションを、VSCodeでデバッグ対象にしている考え方になります。

```python:${workspaceFolder}/app.py
import sys
source = sys.argv[0]
dest = sys.argv[1]
print(f"{source} から {dest} へコピーしました。")
```

```json:${workspaceFolder}/.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [{
        "name": "debug",
        "type": "python",
        "request": "launch",
        "module": "app",                            // python コード app.py
        "console": "integratedTerminal",
        "env": {
            "PYTHONPATH": "${workspaceFolder}",
        },
        "args": [                                   // app.py に与える引数
            "x.txt",
            "x.txt.copy"
        ]
    }]
}
```

```bash:理屈を確認
(venv)$ python -m app x.txt x.txt.copy
x.txt から x.txt.copy へコピーしました。
$
```

このように、実行する側である VSCode の `launch.json` 設定[^2]と、実行される側である python および streamlit の CLI[^3] との対応関係は、それぞれのリファレンスから確認することができます。

[^2]: 実行するコマンドは `launch.json` の `type, module, args` で指定するから。
[^3]: 実行されるコマンド `python -m streamlit run...`の内、`python -m ...`はpython CLI、`streamlit run ...`はStreamlit CLIの機能だから。

* （VSCodeによって）実行されるモノ
  * python のCLI - https://docs.python.org/ja/3.8/using/cmdline.html
  * ライブラリ streamlit のCLI - https://docs.streamlit.io/en/stable/installation.html#import-streamlit
* （デバッグ対象のコードを）実行するモノ
  * VSCode の `launch.json` - https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes
  * VSCodeの拡張機能 の `launch.json`（ここではPython extension for Visual Studio Code） - https://code.visualstudio.com/docs/python/debugging#_debugging-specific-app-types