いものやま。

雑多な知識の寄せ集め

Python製のタスクランナーkumadeを作ってみた。

Python製のタスクランナーを作ってみたので紹介。

スクランナーとは

スクランナーとは、よく行う処理をタスクとして定義しておいて、コマンドラインから簡単に実行できるようにするツール。 ビルドのときに使われることが多いので、ビルドツールと呼ばれることもある。

たとえば、昔からある有名なものだと、GNU Makeとか。 あるいはRubyだとRakeが有名で、これはRubyの記述力の高さを十二分に活かしたものとなっている。

このタスクランナーの便利さは使ってみると分かって、とくにタスクやそれによって生成されるファイルに複雑な依存関係があったりした場合、依存関係を考慮して実行する処理を判断して必要な処理だけやってくれるのがとてもいい。 あるいは、複雑なコマンドを覚えたり、それを正しい順番で呼び出したりといったことも不要になるのもいい。

Pythonでのタスクランナー事情

そんなタスクランナーなんだけど、調べてみるとPythonでいい感じのタスクランナーがなかったりした。

たとえばすぐ見つかるのはInvokeというツールなんだけど、これはファイルの存在や依存関係を見てタスクを実行するかどうか判断する機能がなかった。 また、デコレータをつける関数の名前がそのままタスク名となるので、タスク名のつけ方の自由度が低く、動的にタスクを定義したり、依存関係の指定に難があったりする。

あとtaskipyというツールも名前を聞いたことがあったんだけど、これはpyproject.tomlにタスクを定義するのでPythonを使ってタスク定義を書くことができず、表現力がとても弱い。 簡単なタスクを定義するくらいならいいけど、複雑なことをやるには力不足。

Luigiというツールもあるんだけど、これはタスク定義にクラスを書く感じになっていて、やりたいことに対して書かないといけないことが大袈裟すぎる感じ。 使い始めるまでの学習コストが大きすぎるし、タスク定義を書くのが大変すぎる。

他にもGird、navio-builder、yatte、make.py、Pakeとかが見つかったけど、どれもイマイチ。 これだけいろいろあるのに・・・

そんなわけで、自分が満足できるレベルのものを自分で作ることにした。

kumade

ツールの名前はkumade(熊手)。 これはRubyのRakeから連想したもので、Rakeの意味は熊手だから。

特徴は以下:

  • タスクをKumadefile.pyPythonコードで定義する
    • タスク定義は関数定義にデコレータをつけるだけなので簡単
    • YAMLやTOMLで設定を書くのではなくPythonのコードで書けるので記述力が高い
  • 定義したタスクをコマンドラインからkumadeコマンドで実行できる
  • タスクの依存関係を指定できて、それを考慮した順番でタスクが実行される
  • ファイルを生成するタスクの場合、ファイルが存在するかどうか、依存するファイルよりタイムスタンプが古いかを確認し、実行するかどうか決定する

使い方

インストールはpipコマンドを叩くだけ:

$ pip install kumade

タスクを定義するにはKumadefile.pyを用意して、デコレータを使う:

import kumade as ku  # ライブラリのインポート

@ku.task("taskname")  # 指定された名前のタスクを定義
def do_something() -> None:
    # タスクでやる処理
    ...

@ku.file(Path("hoge.txt"))  # ファイルを作るタスクを定義
def create_hoge_txt() -> None:
    # hoge.txtを作成/更新する処理
    ...

一例は以下:

import kumade as ku

from pathlib import Path
import subprocess

ku.set_default("greet")  # デフォルトのタスクの指定

help_file = Path("help.txt")

@ku.task("greet")
@ku.depend(help_file)  # 依存するタスクの指定
def greeting() -> None:
    print("Hi, this is Kumade.")
    print(f"See {help_file}.")

@ku.file(help_file)
def create_help_file() -> None:
    with help_file.open("w") as outfile:
        subprocess.run(["kumade", "-h"], stdout=outfile)

ここでは"greet"というタスクと"help.txt"を作成するタスクを定義している。 そしてデフォルトのタスクとして"greet"を指定し、また"greet"の依存するタスクとして"help.txt"の生成を設定している。

こうしたKumadefile.pyが用意できたら、あとはkumadeコマンドを叩くだけ。

$ kumade [<taskname> ...]

これで指定されたタスク(指定がなければデフォルトのタスク)が実行される。

たとえば上記のKumadefile.pyを用意してkumadeコマンドを実行すると、kumadeはデフォルトの"greet"タスクを実行しようとする。 このとき、"greet"タスクは"help.txt"ファイルに依存しているので、kumadeは"help.txt"が存在するかをチェック。 これでもし"help.txt"が存在しなかった場合、kumadeは"help.txt"を生成する処理を実行し、それが終わったあとに"greet"を実行する。

そんな感じで、ファイルの存在やタイムスタンプ、依存関係をチェックしながら、実行が必要なタスクを洗い出し、適切な順番でタスクを実行してくれるようになっている。

kumadeはkumadeの開発自体にも使っていて、リポジトリKumadefile.pyには開発用のタスクも記述してある。

これによって、リンタやフォーマッタ、単体テストカバレッジ取得、さらにはパッケージのビルドとアップロードが、kumadeコマンド1つで扱えるようになっている。 便利。

残タスクとして、ドキュメントの整備がまだ残ってるんだけど、なかなかいいものができたと思うので、ぜひ使ってみてほしい。

今日はここまで!