Jupyter Notebookでモジュールをデバッグする（Visual Studio Code）

Visual Studio Codeを使って、ローカルのJupyter ServerにアタッチしてPythonモジュール（.pyファイル）をデバッグする方法を説明します。前半は[VS Codeからリモートサーバー上のJupyterLabにアタッチしてデバッグを行う手順](https://qiita.com/nognog/items/3b9215509c065fb82e2a)を参考にさせていただきました。

# 1. アタッチって？
Visual Studio Codeのデバッグには2種類があります:

- **起動（Launch）**: デバッガーがプロセスを起動し、その上でデバッグを行う
- **接続（Attach）**: 既に実行しているプロセスにアタッチし、その上でデバッグを行う

今回は先にJupyter Notebookを開き、そのサーバーにアタッチしてデバッグしますので、後者になります。アタッチによるデバッグの詳細は[こちら](https://daeudaeu.com/opencv_debug/#launchjson)や[こちら](https://qiita.com/nognog/items/3b9215509c065fb82e2a#1-%E3%81%93%E3%81%AE%E6%8A%95%E7%A8%BF%E3%81%A7%E8%AA%AC%E6%98%8E%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8)で丁寧に解説されています。

# 2. 手動でアタッチ
## 2.1. フォルダー構成

```:フォルダー構成
project_root
├ .vscode
│  └launch.json
├ mymodule.py
└ notebook.ipynb
```

`%autoreload 2`はモジュールの更新を自動で取り込むためのマジックコマンドです。詳しくは[こちら](https://qiita.com/Accent/items/f6bb4d4b7adf268662f4)を参照。モジュールの修正で試行錯誤するとき入れておくと、修正が直ちに反映されるので便利です。

```python:notebook.ipynb（"#%%"ごとに別のセルになっているとみなしてください）
#%%
%load_ext autoreload
%autoreload 2
import os
os.getpid()

#%%
import mymodule
mymodule.double_sum(1,2)
```

```python:mymodule.py
def double_sum(x, y):
    z = (x + y) * 2
    return  z
```


launch.jsonは、`Ctrl+Shift+D` -> `launch.json ファイルを作成します`をクリック -> `Python` -> `Attach using Process ID`で自動で作成されます:

```json:launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Attach using Process Id",
            "type": "python",
            "request": "attach",
            "processId": "${command:pickProcess}"
        }
    ]
}
```

## 2.2. デバッグの実行
`notebook.ipynb`ファイルと`mymodule.py`ファイルをVSCodeで開きます。ノートブックの1つめのセルを実行するとPIDが表示されます。
![2.2.1. Jupyter実行.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/c9fc05d6-ec50-2118-9c00-ce8c15affbba.jpeg)

[F5]でデバッグを開始します。"Select the process to attach to"と表示されますので先ほどのPIDを入力しましょう。
![2.2.2. attach to process.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/70157c51-9d9b-22de-319d-dd5ce61a9d4f.jpeg)

一番下のステータス バーの色が青からオレンジに変わり、`mymodule.py`ファイル側にコネクター（赤い丸で囲ったとこ）みたいなアイコンを含むデバッグ ツールバーが表示されます。3行目にブレークポイントを設定してみましょう。
![2.2.3. mymodule.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/81223500-6dc2-2a34-36e9-1d95628c3bc7.jpeg)

Jupyterノートブックに戻り、2つめのセルを実行すれば、先ほどのブレークポイントで止まります。
![2.2.4. debug.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/7455eb47-0f28-5207-f522-eb4ed2f79a5e.jpeg)


# 3. 自動でアタッチ
ここまでだとつまらないので、もう一つネタを追加します。

正直、PIDを毎回入力するのは面倒ですね。以下で、この作業を自動化していきます。Windows限定ですが、ちょちょいと修正すればLinuxやmacOSでも動かせると思います。

## 3.1. 拡張機能Tasks Shell Inputのインストール
`launch.json`内でコマンドの結果を利用できるようにするため、[VSCode拡張機能 - Tasks Shell Input](https://marketplace.visualstudio.com/items?itemName=augustocdias.tasks-shell-input)をインストールします。

## 3.2. ファイルの修正

`notebook.ipynb`ファイルは、1つめのセルを修正し、PIDを`%LOCALAPPDATA%\Temp\pid.txt`（＝`C:\Users\ユーザー名\AppData\Local\Temp\pid.txt`）に出力させるようにします。

```python:notebook.ipynb（"#%%"ごとに別のセルになっているとみなしてください）
#%%
%load_ext autoreload
%autoreload 2
import os
with open(f'{os.environ["LOCALAPPDATA"]}\\Temp\\pid.txt', mode='w') as f:
    f.write(f'{os.getpid()}')

#%%
import mymodule
mymodule.double_sum(1,2)
```

`launch.json`ファイルは、`configurations`に`"Python: Auto Attach"`を追加します。先ほどとの違いは、PIDをcommandタイプのinput変数`readPID`から取得している点です。`readPID`は、拡張機能Tasks Shell Input（`"shellCommand.execute"`の部分）を使って、コマンドプロンプトの`type`コマンドでファイルからPIDを取得します。

```json:launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Attach using Process Id",
            "type": "python",
            "request": "attach",
            "processId": "${command:pickProcess}"
        },
        {
            "name": "Python: Auto Attach",
            "type": "python",
            "request": "attach",
            "processId": "${input:readPID}"
        }
    ],
    "inputs": [
        {
            "id": "readPID",
            "type": "command",
            "command": "shellCommand.execute",
            "args": {
                "command": "type ${env:LOCALAPPDATA}\\Temp\\pid.txt",
                "useFirstResult": "True",
                "fieldSeparator": " ",
            }
        }
    ]
}

```

## 3.3. デバッグの実行
先ほどと同じくまずは1つめのセルを実行します。そして、`Ctrl+Shit+P` -> `Debug: 選択してデバッグを開始` -> `Python: Auto Attach` でデバッガーを起動します。今度はPIDを入力せずにアタッチすることができました。
![3.3.1. コマンドパレット.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/6e5da408-2e3e-694d-9126-173e60add66d.jpeg)

![3.3.2. auto attach.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/9a42e896-d4ea-7902-61c5-cdca77785e6d.jpeg)



![3.3.3. breakpoint.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/b8f37d37-e7fb-1c02-b43f-97eea5fe494d.jpeg)

# 4. もっと自動でアタッチ
PIDを`notebook.ipynb`の1つめのセルで出力していましたが、ipythonのstartup機能（詳しくは[こちら](https://recruit-tech.co.jp/blog/2018/10/16/jupyter_notebook_tips/#b12)）を使えばその部分を`notebook.ipynb`から除くことができます。

`%USERPROFILE%\.ipython\profile_default\startup`フォルダー（＝`C:\USERS\ユーザー名\.ipython\profile_default\startup`フォルダー）に、以下の内容の`00-vscode_attach_PID.ipy`ファイルを作成します。このフォルダーは特殊なフォルダーで、Notebook起動時にこのフォルダー内のスクリプト（.pyファイル）が自動的に実行されます（[公式](https://ipython.readthedocs.io/en/stable/interactive/tutorial.html#startup-files)または[こちら](https://recruit-tech.co.jp/blog/2018/10/16/jupyter_notebook_tips/#b12)を参照）。

```python:00-vscode_attach_PID.ipy
%load_ext autoreload
%autoreload 2

import os
from pathlib import Path


pid_txt = Path(f'{os.environ["LOCALAPPDATA"]}\\Temp\\pid.txt')
next_pid_txt = Path(f'{os.environ["LOCALAPPDATA"]}\\Temp\\next_pid.txt')

if next_pid_txt.exists():
    next_pid_txt.replace(pid_txt)

with open(next_pid_txt, mode='w') as f:
    f.write(f'{os.getpid()}')
```
PIDをファイル出力しているだけですが、なぜかVisual Studio Code上のJupyter Notebookはひとつ前のタイミングで出力したPIDになる（[何を言ってるのか わからねーと思うが](https://dic.pixiv.net/a/%E3%81%82%E2%80%A6%E3%81%82%E3%82%8A%E3%81%AE%E3%81%BE%E3%81%BE%E4%BB%8A%E8%B5%B7%E3%81%93%E3%81%A3%E3%81%9F%E4%BA%8B%E3%82%92%E8%A9%B1%E3%81%99%E3%81%9C%21)）ので、変なことをしています。（JupyterLabで`.ipynb`ファイルを開く場合には、前と同じコードで大丈夫でした。）

あとは同じです。`notebook.ipynb`の1つめのセルが無くても、デバッグ&アタッチが可能になりました。


# 5. やり残し
以下が気になるけどもういいや('A`)

- 本当は拡張機能Tasks Shell Inputを使わずに素のvscodeだけでやりたかった
- コマンドプロンプトじゃなくてPowerShellを使いたかった

# 参考

- [VS Codeからリモートサーバー上のJupyterLabにアタッチしてデバッグを行う手順](https://qiita.com/nognog/items/3b9215509c065fb82e2a)
- [VSCode で Python と OpenCV を混合でデバッグ（ステップ実行）する方法！](https://daeudaeu.com/opencv_debug/)
- [公式ドキュメント - Variables Reference](https://code.visualstudio.com/docs/editor/variables-reference)
- [VSCode拡張機能 - Tasks Shell Input](https://marketplace.visualstudio.com/items?itemName=augustocdias.tasks-shell-input)
- [Visual Studio Codeでデバッグの引数を実行の度に入力したり選択したりする](https://hachian.com/2020/08/24/vscode-inputs-launch-json/)
- [あなたの生産性を向上させるJupyter notebook Tips](https://recruit-tech.co.jp/blog/2018/10/16/jupyter_notebook_tips/)