ヒトリ歩き

愚痴とかいろいろ書きます

Pythonのclick.commandを使えば簡単!コマンドの実装方法

シェルスクリプトで頑張って作っていたパラメータの解析もPythonのclick.commandを使えば簡単に実装できるし、コマンドで必要なことがモジュールとして提供されている。
シェルスクリプトから脱却して、Pythonでコマンドを作ろうぜ。

フォントの色、スタイルを変更できる

style()関数を使用することで、フォントの色やスタイルを変更が可能。

click.echo(click.style('Hello World!', fg='green'))
click.echo(click.style('Some more text', bg='blue', fg='white'))
click.echo(click.style('ATTENTION', blink=True, bold=True)) 

ヘルプテキストの記載が簡単

シェルスクリプトのヘルプメッセージは地味に大変。
click.commandだと関数のdocstringが指定されている場合、自動的にdocstringの内容が使用される。

@click.command()
@click.argument("name")
def hello(name: str):

    """docstringの内容がヘルプに指定されるよ"""

    click.echo("name: " + name )
Usage: clickcmd_sample.py [OPTIONS] NAME

  docstringの内容がヘルプに指定されるよ

Options:
  --help  Show this message and exit.

環境変数

auto_envvar_prefixに環境変数プレフィックスを設定する必要がある。
auto_envvar_prefixに指定したプレフィックスとアンダーバーを除いた文字列が変数名になる。
以下の場合、HELLO_USERNAMEとHELLO_JOBが環境変数となり、環境変数の値が格納される変数名は、usernameとjobになる。

@click.command()
@click.option('--username')
@click.option("--job", envvar="JOB")
def hello(username, job):

    click.echo("username = " + str(username))
    click.echo("job      = " + str(job))

if __name__ == "__main__":
    hello(auto_envvar_prefix="HELLO")
# export HELLO_USERNAME=sato
# export HELLO_JOB=engineer 
# python clickcmd_sample.py 
username = sato
job      = engineer

ユーザー入力のためのプロンプト表示も

コマンドのパラメータ指定ではなく、ユーザーに値を入力させたいケースがある。
そのケースもclick.commandは対応している。
click.optionデコレーターのpromptパラメータにTrueを指定するだけ。

import click

@click.command()
@click.option('--brithplace', prompt=True)
def hello(brithplace):

    click.echo("brithplace : " + str(brithplace))

if __name__ == "__main__":
    hello()

プロンプトのメッセージもカスタマイズをする場合、promptパラメータに表示したいメッセージを指定する。

import click

@click.command()
@click.option('--brithplace', prompt="あなたの出身地は?")
def hello(brithplace):

    click.echo("入力した出身地 : " + str(brithplace))

if __name__ == "__main__":
    hello()
あなたの出身地は?: 神奈川
入力した出身地 : 神奈川

パスワード入力も簡単に実装できる。
hide_inputパラメータをTrueに設定し、入力を非表示にする。
また、confirmation_promptで確認用に再入力をさせる。
試してみて分かったことは、パスワード入力を数回間違えれば、コマンドが終了するのではなく、ずっとパスワード入力が求められるということ。
回数指定出来たらいいのに。

import click

@click.command()
@click.option('--password',
              prompt="パスワードを入力してください",
              hide_input=True,
              confirmation_prompt=True)
def hello(password):

    click.echo("入力したパスワード : " + str(password))

if __name__ == "__main__":
    hello()

テストも簡単

テスト用のモジュールも提供されているので、テストが容易。
以下をテストをする。

import click

@click.command()
@click.argument("brithplace")
@click.argument("schoolname")
def show_your_birthplace(brithplace, schoolname):
    click.echo("brithplace = " + str(brithplace))
    click.echo("schoolname = " + str(schoolname))

if __name__ == '__main__':
    show_your_birthplace()

CliRunnerクラスのオブジェクトを生成し、invoke関数を実行する。
第一パラメータにテスト対象の関数、第二パラメータにパラメータを配列で指定。
invoke関数の戻り値に結果が格納される。
標準出力はresult.outputに格納される。改行含めての文字列になっているので、assertするときは改行で分割したほうが、チェックしやすい。

from click.testing import CliRunner
from click_easy import show_your_birthplace

def test_show_your_birthplace():
    runner = CliRunner()
    result = runner.invoke(show_your_birthplace, ["Kanagawa", "WAHAHA"])
    assert result.exit_code == 0
    assert result.output == "brithplace = Kanagawa\nschoolname = WAHAHA\n"

ユーザーの入力させる処理もinput変数を使用すればテスト可能。
これは嬉しい。

import click

@click.command()
@click.option("--brithplace", prompt="出身は?")
@click.option("--schoolname", prompt="出身校は?")
def show_your_birthplace(brithplace, schoolname):
    click.echo("brithplace = " + str(brithplace))
    click.echo("schoolname = " + str(schoolname))

if __name__ == '__main__':
    show_your_birthplace()

input変数に入力する値を設定。
output変数には入力を求める文字列も含まれるので、合わせて試験が可能。

from click.testing import CliRunner
from click_easy import show_your_birthplace

def test_show_your_birthplace():
    runner = CliRunner()
    result = runner.invoke(show_your_birthplace, input="Kanagawa\nWAHAHA\n")
    assert result.exit_code == 0
    #assert result.output == "出身は?: Kanagawa\n出身校は?: WAHAHA\nbrithplace = Kanagawa\nschoolname = WAHAHA\n"
    outputs = result.output.split('\n')
    assert len(outputs)-1 == 4 # 末尾の改行を除く
    assert outputs[0] == "出身は?: Kanagawa"
    assert outputs[1] == "出身校は?: WAHAHA"
    assert outputs[2] == "brithplace = Kanagawa"
    assert outputs[3] == "schoolname = WAHAHA"
    assert outputs[4] == ""

さいごに

コマンドはほぼシェルスクリプトで作成しているので、単体テストが後回しになったり、全ルートを通すために苦労する。
それにシェルスクリプトのテストツールも少ない。そもそも、シェルスクリプトのテストツールを使っているところ自体が少ない。
それに比べて、Pythonでコマンドに必要なことがモジュールで提供されているので、実装も楽だし、テストも容易。これはPythonでコマンド作ろぜってことだ。
自分のプロジェクトでも機会があれば、Pythonでコマンドを作ろうと思う。

参考

click.palletsprojects.com

kotapontan.hatenablog.com