🥺

[python] invokeのtasks.pyをVSCodeでデバッグする

2024/07/05に公開

invokeのタスク(tasks.py)をVSCodeでデバッグしたかったのでやりかたの備忘録

参考:
https://zenn.dev/shun_kashiwa/articles/debug-python-cli-with-debugpy-vscode

最初にかんたんまとめ

  • 必要な拡張機能入れる(Python拡張入れとけば十分)
  • launch.jsonのmoduleinvokeを記述する, argsで呼び出したいtask名を指定する
    launch.json
    {
      "version": "0.2.0",
      "configurations": [
        {
          "name": "Debug invoke task: my-task",
          "type": "debugpy",
          "request": "launch",
          "module": "invoke",
          "args": ["my-task"],
          "cwd": "${workspaceFolder}"
        }
      ]
    }
    
  • パラメータを可変にしたければinputsをlaunch.jsonに定義する

debugpy使ってデバッグ

debugpyを起動しておいて、VSCodeのデバッガを起動するとdbugpyにアタッチするような構成にする

0. 準備

  • VSCodeでPython Debugger拡張入れておく
    • python拡張を入れるとPylanceと一緒に自動でインストールされる
  • debugpyとinvokeをインストール
    pip install debugpy invoke
    

1. launch.jsonに構成追加

launch.jsonに以下を追記

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug invoke task",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
    }
  ]
}

2. debugpy起動してinvoke呼び出す

$ python -m debugpy --wait-for-client --listen 5678 -m invoke my-task

--wait-for-clientでVSCodeがattachするのを待ってくれる

3. VSCodeのデバッグを実行

VSCodeの「実行とデバッグ」から前段で作った構成("Debug invoke task")を選択してデバッグを開始(F5)すると
3で起動したdebugpyのプロセスにアタッチされる
invokeのtask.pyでブレークポイントが設定されていればそこで止まってくれる

改良

まいどCLIでdebugpyを呼び出すのは大変。なので,VSCodeのTaskでdebugpyを起動させ、その後デバッグを自動で実行させる

tasks.json

tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "pre launch: invoke my-task",
      "type": "shell",
      "command": "python -m debugpy --wait-for-client --log-to-stderr --listen 5678 -m invoke my-task",
      "isBackground": true,
      "problemMatcher": {
        "owner": "custom",
        "pattern": {
          "regexp": "^$"
        },
        "background": {
          "activeOnStart": true,
          "beginsPattern": ".*",
          "endsPattern": "wait_for_client()"
        }
      }
    }
  ]
}

--log-to-stderrをつけておくとログ出力をしてくれて、クライアントの呼び出し準備が完了すると末尾に"wait_for_client()"と出力されるのでそれを機にバックグラウンドに移行するようにする

launch.json

launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug invoke task: my-task",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "preLaunchTask": "pre launch: invoke my-task", // これを追記
    }
  ]
}

preLaunchTaskで作成したタスクを指定する。
これでデバッグ実行するとまずタスクが呼び出されてdebugpy起動した後にデバッグ実行がアタッチしてくれる

最終形

こんなことしなくとも launch.jsonでmoduleを指定すればよかった

https://code.visualstudio.com/docs/python/debugging#_module

Provides the ability to specify the name of a module to be debugged, similarly to the -m argument when run at the command line. For more information, see Python.org

要するにlaunch.jsonでmoduleinvokeと指定すればdebugpy -m invokeと同等のことをVSCode側が良しなにやってくれる
これによりpreLaunchTaskでtaskを実行する必要がなくなった

launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug invoke task: my-task",
      "type": "debugpy",
      "request": "launch",
      "module": "invoke",
      "args": ["my-task"],
      "cwd": "${workspaceFolder}"
    }
  ]
}

さらに任意のinvoke taskやパラメータを指定できるようにする
tasks.pyは以下のように書くとする

tasks.py
from invoke.collection import Collection
from invoke.tasks import task


@task
def my_task_a(c):
    print("my task a")


@task
def my_task_b(c):
    print("my task b")


@task
def my_task_c(c, name):
    print(f"my task c with {name}")


@task
def main(c, task_name, param=None):
    if task_name == "my-task-a":
        my_task_a(c)
    elif task_name == "my-task-b":
        my_task_b(c)
    elif task_name == "my-task-c" and param:
        my_task_c(c, name=param)
    else:
        print(f"No idea what '{task_name}' is!")


namespace = Collection()
namespace.add_task(main)  # type: ignore

my-task-a~my-task-cを指定して実行するのではなく、常にmainタスクを呼び出して
その引数で個別のどのタスクを実行するのか指定する

launch.json
{
  // IntelliSense を使用して利用可能な属性を学べます。
  // 既存の属性の説明をホバーして表示します。
  // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python デバッガー: モジュール",
      "type": "debugpy",
      "request": "launch",
      "module": "invoke",
      "args": ["main", "--task-name=${input:task_name}", "--param=${input:any_param}"],
      "cwd": "${workspaceFolder}"
    }
  ],
  "inputs": [
    {
      "type": "pickString",
      "description": "select task",
      "id": "task_name",
      "options": [
        "my-task-a",
        "my-task-b",
        "my-task-c"
      ]
    },
    {
      "type": "promptString",
      "description": "any parameter",
      "id": "any_param",
      "default": ""
    }
  ]
}

共通のtask mainを先にかましてるのはパラメータを統一しとかないと呼びわけが難しそうだったから

まとめ

  • VSCodeがdebugpyを使って裏でよしなにやってくれてる流れが理解できた
  • ちなみにpytest使ってるときにデバッグするときはカバレッジ計測オフにしないとブレークポイント止まってくれないので注意

Discussion