いものやま。

雑多な知識の寄せ集め

Python製のタスクランナーkumadeをバージョンアップした。

Pythonでいい感じに使えるタスクランナーがなかったので自作したのがkumade。

今回、いくつかの機能を追加実装してバージョンアップした。

追加実装した機能

kumadeはデータ分析、アルゴリズム開発の実務でも使ってたりするんだけど、実際に使ってるといくつかほしい機能が出てきた。 今回実装したのはそんな機能。

  • Pythonモジュールのインポート容易化(0.2.0以降)
  • 設定値の実行時指定(0.2.0以降)
  • タスクの並列実行(0.3.0以降)

Pythonモジュールのインポート容易化

タスクはKumadefileに書いていくけど、ベタに全部書くとメンテナスが大変になる。 そこでコアな機能は別のファイルに書いておいて、インポートして使うことが考えられる。

ただ、そのときにちょっと厄介なのがパスの解決。 カレントディレクトリに依存せずにインポートできるようにするには、パスを追加しておく必要があった:

# Kumadefile.py

import sys
from pathlib import Path

# Kumadefile.pyのあるディレクトリをパスに追加
project_dir = Path(__file__).parent
sys.path.append(str(project_dir))

from some_module import some_function

...

このパスの追加は一手間だし、リンタとしてFlake8を使っているとE402という指摘を受けてしまう。

そこで、Kumadefileのあるディレクトリが自動的にパスに追加されるように変更した。 これによりインポートが簡単にできるようになっている:

# Kumadefile.py

# 0.2.0以降はパスの追加なしでインポート可能
from some_module import some_function

...

これでE402の指摘も出なくなっている。

設定値の実行時指定

タスクの挙動を実行時に微調整したいということがある。

簡単な例だと、単体テストを実行するときに冗長出力させるかどうかとか:

# 冗長出力しない場合は単にunittestを実行
$ python -m unittest
...........................................................................
----------------------------------------------------------------------
Ran 75 tests in 0.639s

OK

# 冗長出力したい場合は-vオプションをつけて実行
$ python -m unittest -v
test_create (tests.concurrent.test_concurrent_task_runner.TestConcurrentTaskRunner.test_create) ... ok
...(省略)...
test_set_default (tests.test_utility.TestUtility.test_set_default) ... ok

----------------------------------------------------------------------
Ran 75 tests in 0.659s

OK

一つの方法としては、それぞれタスクを用意するというのが考えられる:

@ku.task("test")
@ku.help("Run unittest.")
def test() -> None:
    subprocess.run(["python", "-m", "unittest"])

@ku.task("verbose_test")
@ku.help("Run unittest with verbose output.")
def verbose_test() -> None:
    subprocess.run(["python", "-m", "unittest", "-v"])

# 単にテストしたい場合は
# $ kumade test
# 冗長出力ありでテストしたい場合は
# $ kumade verbose_test
# を実行する

ただ、条件がいろいろあったりすると定義するタスクの数は組み合わせ的に増えていく可能性があるし、他のタスクから依存されて実行される場合には実行する内容を切り替えたりできない問題がある。

そこで、設定項目を定義しておいて、実行時に指定できるようにした。 この例だと以下のように書くことができるようになった:

# boolの設定項目を追加(デフォルト値はFalse)
ku.add_bool_config(
    "test_verbose",
    "Run unit test with verbose output if true.",
)

@ku.task("test")
@ku.help("Run unittest.")
def test() -> None:
    # 設定を取得し、値を参照して処理を行う
    config = ku.get_config()
    if config.test_verbose:
        subprocess.run(["python", "-m", "unittest", "-v"])
    else:
        subprocess.run(["python", "-m", "unittest"])

# 単にテストしたい場合は
# $ kumade test
# 冗長出力ありでテストしたい場合は
# $ kumade test_verbose=true test
# を実行する

感じとしてはMakefile環境変数を指定して実行するのに似ている。 ただ、kumadeでは設定項目の型を指定できるようにしたので、参照時に変換を自前で行うのが不要になっていたり、タスク一覧の表示で設定項目の一覧も見れたりと、利便性が高まってる:

# -tオプションで設定項目とタスクの一覧を表示
$ kumade -t
Configuration items:
  test_verbose  # Run unit test with verbose output if true. (default: False)
  ...
Tasks:
  test                   # Run unittest.
  ...

タスクの並列実行

データ分析だと、インプットとなるファイルのダウンロード、機械学習での処理、処理結果の可視化などのタスクを依存関係で繋いでいく感じになるんだけど、処理する対象が多いと並列実行したいという要望が出てくる。 というか、実際に出てきた。

そこで、MakeやRakeと同様に-jオプションでタスクを並列実行できるようにした:

# 並列実行する場合は-jオプションで並列数を指定
$ kumade -j 3 some_task

並列実行するときに標準出力を使って場合、適切に排他処理をしないと出力が入り混じってしまうというのがあるけど、kumadeではワーカーごとの出力が分かるようにしている:

# カウントダウンするタスクの実行例
$ kumade -j 3 countdown
[Worker0] 5...
[Worker2] 4...
[Worker1] 3...
[Worker0] 2...
[Worker2] 1...
[Worker1] 0!

なので、単一プロセスで実行する場合も並列処理する場合も基本的にはKumadefileを書き換える必要がないようにしている。 もちろん、標準出力以外へのアクセスは排他されないので、必要に応じて対策が必要だけど。


ソースコードGitHubで見れるので、興味ある人はぜひ。

今日はここまで!