JX通信社アドベントカレンダー2日目を担当します、TatchNicolasと申します。
今年(2018年)の8月に入社し、主にサーバサイドの開発を担当しています。
いきさつ
Pythonの環境分離にはいろいろな方法がありますが、私はJediでの補完が効かせやすく、環境の作り直しも簡単なPipenv(+Pyenv)を使っています。
しかしPipenvの独自コマンド定義機能であるscripts
では、 Node.jsのpackage.jsonのように&&
によるコマンドの連続実行が定義できません。
https://pipenv.readthedocs.io/en/latest/advanced/#custom-script-shortcuts
list表記を使うなどして複数コマンドの一括実行を実装してほしいという声もあるのですが、なかなか実現しなさそうな様子です。
(GitHub Issue "How to combine multiple command in one script?")
ならば古き良きMakefileで!とも考えたのですが、普段書き慣れていない構文を毎度調べる・思い出すのも大変です。
そこで、上記IssueのなかでInvokeなるものが言及されていましたので、紹介したいと思います。
Invokeとは
Pythonでいろいろなタスクを定義できるライブラリで、コマンドラインから操作するタスクを手軽に定義できます。
InvokeとPipenvと組み合わせて何が嬉しいか
- タスクの実行環境自体をPipenvに内包できる
- Djangoやgunicornのコマンドをその依存関係とセットでPipenvの環境内に持つことが出来る
- Pythonの豊富なライブラリを使える
- 書き慣れたPythonの構文で様々なタスクを定義できる
- 文字列操作、ループやIFの条件を気軽に書ける
- コマンドを
pipenv run <task name>
の形式から、inv <task name>
の形式へと短くできる-
pipenv shell
で仮想環境をオンにしている前提です - 仮想環境の外からなら、
pipenv run inv <task name>
の形式で呼び出せます。
-
- Invokeのタスクは別ファイルに定義されるので、Pipfileが散らからない
やってみる
記事公開時の実行環境
- Ubuntu 18.04
- Python 3.7.0
- Pipenv 2018.7.1
Pipenvの導入、概要については公式ドキュメントをご参照ください。
invokeのインストール
Pipenvを使ってdev-packages
にインストールします。
$ pipenv install --dev invoke
タスクの定義
tasks.py
というファイルを作成し、その中にタスクを定義していきます。
ターミナル上のコマンドはinvoke.run()
という関数の中に文字列で書きます。
import invoke
@invoke.task
def hello(c):
invoke.run('echo "hello invoke!"')
タスクの実行
invoke foo
で、上記tasks.pyで定義した関数名のタスクを実行できます。
試しにhelloを動かしてみます。
# 仮想環境の外から呼ぶ
$ pipenv run invoke
hello invoke!
# 仮想環境の中から呼ぶ
$ invoke hello
hello invoke!
invoke foo
はinv foo
と短く書けます。たった三文字なので、pipenv run foo
よりもちょっと楽ですね。
以降は仮想環境に入っている想定で、また短くinv
で例を書きます。
いろいろな使い方
先程のtasks.py
を編集してみます。
import invoke
@invoke.task
def hello(c, name):
print(f'name is {type(name)}')
invoke.run(f'echo "hello {name}!"')
@invoke.task
def run_webserver(c, port=8000):
'''
http.serverを実行します
'''
print(f'port is {type(port)}')
invoke.run(f'python -m http.server {port}', pty=True)
コマンド一覧やヘルプの表示
inv --list
で定義されているタスクの一覧を、inv --help <タスク名>
で各タスクのヘルプを参照できます。
$ inv --list
Available tasks:
hello
run-webserver http.serverを実行します
$ inv --help hello
Usage: inv[oke] [--core-opts] hello [--options] [other tasks here ...]
Docstring:
none
Options:
-n STRING, --name=STRING
$ inv hello --name John
name is <class 'str'>
hello John!
$ inv --help run-webserver
Usage: inv[oke] [--core-opts] run-webserver [--options] [other tasks here ...]
Docstring:
http.serverを実行します
Options:
-p INT, --port=INT
$ inv run-webserver -p 8888
port is <class 'int'>
Serving HTTP on 0.0.0.0 port 8888 (http://0.0.0.0:8888/) ...
タスクに引数を渡す
上記の例のとおり、定義したタスクの関数に引数を定義することで、タスクはコマンドライン引数を受け付けるようになります。
また、デフォルト値を指定すると型をよしなに変換してくれます。hello
のオプションname
はデフォルト値を指定していないのでヘルプでもSTRINGを想定しており、run-webserver
のオプションport
はINTを想定しており、タスク内でもそう扱われていることが分かります。
invoke.run()
にpty=True
を指定することでptyが有効になり、たとえばdocker-compose exec <コンテナ名> bash
のようにそのままコンテナ内に入ったりできるようになります。
タブ保管用のスクリプトを生成する
Makefileはそのままでもタブ補完が使えますが、Invokeでもinvoke --print-completion-script zsh
を実行した結果を.zshrc
に入れるなり、別ファイルに書き出してsource
するなりしてタブ補完が効くようになります。
タスクの依存関係
あるタスクの前に呼びたいタスクを指定することも出来ます。tasks.pyに追記してみます。
@invoke.task
def lint(c):
invoke.run('pylint some_package')
@invoke.task
def test(c):
invoke.run('coverage -m pytest')
@invoke.task(lint, test)
def package(c):
invoke.run('python setup.py sdist bdist_wheel')
上記の例では、inv package
を実行すると、その前にlintとtestが走ります。
出力や終了コードを利用する
invoke.run()
はstdout/stderrの内容、コマンドの終了コードなどを返却するので処理分けになどにも使えます。REPLで様子を見てみましょう。
>>> import invoke
>>> result=invoke.run('echo Hello!')
Hello!
>>> result.ok
True
>>> result.stdout
'Hello!\n'
まとめ
タスクが増えてきたらtasks.py
内で関数を定義して処理を共通化したり、namespace
を使ってtasks.py
を複数ファイルに分割したりして、プロジェクトに合わせて拡張していくことができます。上手に使って、積極的に楽をしていきましょう。
たとえばAWSのboto3などと組み合わせてデフォルト値や条件を上手く設定すれば、プロジェクトごとに公式のAWS CLIよりも短く使いやすいコマンドラインツールにすることもできるでしょう。