目次
- 1.はじめに
- 2.コールバックのユースケース
- 入力値の検証と変換
- 動的なデフォルト値の設定
- 相互依存するパラメータの処理
- 3.コールバック関数の引数・返り値
- コールバック関数に何が渡されるか
- コールバック関数から何を返せばよいか
- 4.その他
-
type
を使って入力値の検証と変換を行う -
default
に関数を設定する
-
- 5.まとめ
1.はじめに
Clickは少ないコードできれいなCLIツールを作れるPythonパッケージです。
CLIツールはPython標準モジュールのargparseでも作れますが、Clickのほうが簡単に作れます。
Clickの公式ドキュメントはこちらです。
このドキュメントを読んでいて、パラメータにコールバックを設定できることに気付きました。
しかし、少しドキュメントを読んだだけでは具体的な実装方法が分からなかったので、この記事で理解していこうと思います。
この記事では、「パラメータのコールバックで何ができるのか」、「コールバックをどのように実装するのか」が理解できることをゴールにします。
この記事で書くこと:
- Clickのコールバックのユースケース
- 入力値の検証と変換
- 動的なデフォルト値の設定
- 相互依存するパラメータの処理
- Clickのコールバック関数の引数・返り値
- 何が渡されるのか
- Contextオブジェクト
- Parameterオブジェクト
- value
- 何を返せばよいのか
- 何が渡されるのか
この記事を読むことで、Clickのコールバックで何ができるのかがイメージでき、実際に自分のCLIツールに応用できるようになると思います。
この記事では公式ドキュメントを参考にしています。
記事の中で示すコードは以下のバージョンで実行しました。
$ python -V
Python 3.10.14
$ pip list | grep click
click 8.1.7
2.コールバックのユースケース
3つのユースケースを紹介します。
- 入力値の検証と変換
- 動的なデフォルト値の設定
- 相互依存するパラメータの処理
入力値の検証と変換
コールバック関数を使用して、ユーザーが入力したパラメータ値を検証したり、必要に応じて変換したりできます。
例えば、日付文字列をdatetime
オブジェクトに変換したり、特定の範囲内にある数値かどうかを確認したりするのに役立ちます。
実際にどのような挙動のCLIツールを作れるか見てみましょう。
関数process_date
は
- コマンドラインから文字列を受け取る
- 日付の形式が合っているか検証する (コールバック関数)
- 合っていたら
datetime
オブジェクトに変換する (コールバック関数) - 変換した
datetime
オブジェクトを出力する
ということをします。
このうち2.と3.がコールバック関数validate_date
で実施する部分です。
from datetime import datetime
import click
def validate_date(ctx, param, value):
try:
return datetime.strptime(value, "%Y-%m-%d")
except ValueError:
raise click.BadParameter("日付は YYYY-MM-DD 形式で入力してください。")
@click.command()
@click.option(
"--date",
callback=validate_date,
help="日付 (YYYY-MM-DD)",
)
def process_date(date):
click.echo(f"処理する日付: {date}")
click.echo(f"dateの型 : {type(date)}")
if __name__ == "__main__":
process_date()
日付形式が正常な時の実行例
datetime
型に変換されていることが分かります。
$ python validate_input.py --date 2024-12-31
処理する日付: 2024-12-31 00:00:00
dateの型 : <class 'datetime.datetime'>
日付形式が不正な時の実行例
検証が行われていることがわかります。
$ python validate_input.py --date 2024/12/31
Usage: validate_input.py [OPTIONS]
Try 'validate_input.py --help' for help.
Error: Invalid value for '--date': 日付は YYYY-MM-DD 形式で入力してください。
パラメータに値が指定されなかったとき
上記の例では、パラメータに値が指定されなかったとき、 value=None
となり、 datetime.strptime(value, "%Y-%m-%d")
の部分でTypeError
となります。
TypeError: strptime() argument 1 must be str, not None
これのエラーハンドリングを追加する、もしくはNone
のまま値を受け取って関数process_date
の中で何かしら処理したいということもあると思います。
その簡単な方法はtype
を使って入力値の検証と変換を行うで説明します。
動的なデフォルト値の設定
コールバック関数を使用して、実行時に動的にデフォルト値を設定できます。
例えば、現在の日付や時刻、環境変数の値に基づいてデフォルト値を決定する場合に便利です。(よりシンプルな方法はdefaultに関数を設定するを参照してください。)
実際にどのような挙動のCLIツールを作れるか見てみましょう。
関数process_files
は
- コマンドラインから文字列を受け取る
- パラメータに値が指定されているかを確かめる (コールバック関数)
- 指定されていなかったらカレントディレクトリ下のoutputというディレクトリを指定する (コールバック関数)
- 指定されたディレクトリを出力する
ということをします。
このうち2.と3.がコールバック関数get_default_output_dir
で実施する部分です。
import os
import click
def get_default_output_dir(ctx, param, value):
if value is None:
return os.path.join(os.getcwd(), "output")
return value
@click.command()
@click.option(
"--output-dir",
callback=get_default_output_dir,
help="出力ディレクトリ",
)
def process_files(output_dir):
click.echo(f"ファイルを {output_dir} に出力します")
if __name__ == "__main__":
process_files()
パラメータに値を指定したときの実行例
指定した値が出力されていることがわかります。
$ python dynamic_default_value.py --output-dir some_directory/output
ファイルを some_directory/output に出力します
パラメータに値を指定しなかったときの実行例
カレントディレクトリ下のoutputディレクトリが出力されていることがわかります。
$ pwd
/workspaces
$ python dynamic_default_value.py
ファイルを /workspaces/output に出力します
相互依存するパラメータの処理
複数のパラメータ間に依存関係がある場合、コールバック関数を使用してそれらの関係を管理できます。
例えば、あるパラメータの値に基づいて別のパラメータの振る舞いを変更したり、特定の組み合わせのパラメータが与えられた場合にのみ特定の処理を行ったりすることができます。
実際にどのような挙動のCLIツールを作れるか見てみましょう。
関数process_payment
は
- コマンドラインから文字列を受け取る
-
payment_method=credit_card
のときにカード番号が与えられているかを確かめる (コールバック関数) - 与えられていなかったらエラーを送出する (コールバック関数)
- 問題なければ与えられた値を出力する
ということをします。
このうち2.と3.がコールバック関数validate_payment_info
で実施する部分です。
import click
def validate_payment_info(ctx, param, value):
if ctx.params.get("payment_method") == "credit_card" and not value:
raise click.BadParameter("クレジットカード支払いの場合、カード番号は必須です。")
return value
@click.command()
@click.option(
"--payment-method",
type=click.Choice(
["cash", "credit_card"],
),
required=True,
)
@click.option(
"--card-number",
callback=validate_payment_info,
)
def process_payment(payment_method, card_number):
click.echo(f"支払い方法: {payment_method}")
if payment_method == "credit_card":
click.echo(f"カード番号: {card_number}")
if __name__ == "__main__":
process_payment()
支払い方法をcash
にしたときの実行例
問題なく実行できています。
$ python interdependent_parameters.py --payment-method cash
支払い方法: cash
支払い方法をcredit_card
にしてカード番号を与えないときの実行例
カード番号を与えなかったため、エラーとなっていることがわかります。
$ python interdependent_parameters.py --payment-method credit_card
Usage: interdependent_parameters.py [OPTIONS]
Try 'interdependent_parameters.py --help' for help.
Error: Invalid value for '--card-number': クレジットカード支払いの場合、カード番号は必須です。
支払い方法をcredit_card
にしてカード番号を与えたときの実行例
問題なく実行できました。
$ python interdependent_parameters.py --payment-method credit_card --card-number 1122334455667788
支払い方法: credit_card
カード番号: 1122334455667788
3.コールバック関数の引数・返り値
ここまで具体的なユースケースを見てきて、以下のような疑問が出てきたかもしれません。
これらの疑問を解決していきましょう。
- コールバック関数には何が渡されるの?
-
ctx
ってなに? -
param
ってなに? -
value
ってなに?
-
- コールバック関数からは何を返せばいいの?
コールバック関数に何が渡されるか
コールバック関数に渡されるものには3つあります。
変数名 | オブジェクトの型 | 説明 |
---|---|---|
ctx |
click.Context |
Context はコマンド実行時の状態を保持しているオブジェクトです。大抵の場合は内部的に使われるものです。 ドキュメント
|
param |
click.Parameter |
コールバック関数を設定したパラメータに対応するオブジェクトです。パラメータのメタ情報(デフォルト値、必須かなど)を保持しています。 実際にはこのサブクラスのclick.Option またはclick.Argument を使うことになります。 ドキュメント
|
value |
Any |
コマンドラインから渡された値です。(実際にはtype で指定したクラスによって変換された後の値です。) |
それぞれもう少し詳しく見ていきましょう。
click.Context
以下の peak_ctx
というコールバック関数をパラメータname
に設定して ctx
がどのようなものなのか見てみましょう。
from pprint import pprint
from typing import Any
import click
def peak_ctx(
ctx: click.Context,
param: click.Parameter,
value: Any,
):
print("=" * 5, "(1)", "type of ctx", "=" * 5)
print(type(ctx))
print()
print("=" * 5, "(2)", "vars(ctx)", "=" * 5)
pprint(vars(ctx))
print()
return value
import click
@click.command()
@click.option(
"--count",
default=1,
help="Number of greetings.",
)
@click.option(
"--name",
default="World",
help="The person to greet.",
callback=peak_ctx,
)
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
print("=" * 5, "outputs of hello()", "=" * 5)
for _ in range(count):
click.echo(f"Hello {name}!")
if __name__ == "__main__":
hello()
count
だけ値を指定して実行してみます。
$ python what_is_context.py --count 3
===== (1) type of ctx =====
<class 'click.core.Context'>
===== (2) vars(ctx) =====
{'_close_callbacks': [],
'_depth': 2,
'_exit_stack': <contextlib.ExitStack object at 0x7f02ac053c70>,
'_meta': {},
'_opt_prefixes': set(),
'_parameter_source': {'count': <ParameterSource.COMMANDLINE: 1>,
'help': <ParameterSource.DEFAULT: 3>,
'name': <ParameterSource.DEFAULT: 3>},
'allow_extra_args': False,
'allow_interspersed_args': True,
'args': [],
'auto_envvar_prefix': None,
'color': None,
'command': <Command hello>,
'default_map': None,
'help_option_names': ['--help'],
'ignore_unknown_options': False,
'info_name': 'what_is_context.py',
'invoked_subcommand': None,
'max_content_width': None,
'obj': None,
'params': {'count': 3},
'parent': None,
'protected_args': [],
'resilient_parsing': False,
'show_default': None,
'terminal_width': None,
'token_normalize_func': None}
===== outputs of hello() =====
Hello World!
Hello World!
Hello World!
まず、(1) type of ctx
から、ctx
の型が Context
だと確認できました。
次に、(2) vars(ctx)
から、ctx
はコマンド実行時の様々な状態を保持していることがわかります。たとえば、
'params': {'count': 3}
という部分にコマンドラインから指定したcount
の値である3が入っていることがわかります。また、
'_parameter_source': {'count': <ParameterSource.COMMANDLINE: 1>,
'help': <ParameterSource.DEFAULT: 3>,
'name': <ParameterSource.DEFAULT: 3>},
という部分に、各パラメータのソースの情報が保持されています。例えば、count
はコマンドラインから入力したのでCOMMANDLINE
、name
は与えられていないのでデフォルト値が使われておりDEFAULT
となっています。
このパラメータのソースは取得するためのメソッドがContext
に用意されています。
(ドキュメント: get_parameter_source(name) )
click.Parameter
今度は以下の peak_param
というコールバック関数をパラメータname
に設定して param
がどのようなものなのか見てみましょう。
from pprint import pprint
from typing import Any
import click
def peak_param(
ctx: click.Context,
param: click.Parameter,
value: Any,
):
print("=" * 5, "(1)", "type of param", "=" * 5)
print(type(param))
print("is subclass?:", issubclass(type(param), click.Parameter))
print()
print("=" * 5, "(2)", "vars(param)", "=" * 5)
pprint(vars(param))
print()
return value
import click
@click.command()
@click.option(
"--count",
default=1,
help="Number of greetings.",
)
@click.option(
"--name",
default="World",
help="The person to greet.",
callback=peak_param,
)
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
print("=" * 5, "outputs of hello()", "=" * 5)
for _ in range(count):
click.echo(f"Hello {name}!")
if __name__ == "__main__":
hello()
name
だけ値を指定して実行してみます。
$ python what_is_parameter.py --name John
===== (1) type of param =====
<class 'click.core.Option'>
is subclass?: True
===== (2) vars(param) =====
{'_custom_shell_complete': None,
'_flag_needs_value': False,
'allow_from_autoenv': True,
'callback': <function peak_param at 0x7f00e5b4fd90>,
'confirmation_prompt': False,
'count': False,
'default': 'World',
'envvar': None,
'expose_value': True,
'flag_value': False,
'help': 'The person to greet.',
'hidden': False,
'hide_input': False,
'is_bool_flag': False,
'is_eager': False,
'is_flag': False,
'metavar': None,
'multiple': False,
'name': 'name',
'nargs': 1,
'opts': ['--name'],
'prompt': None,
'prompt_required': True,
'required': False,
'secondary_opts': [],
'show_choices': True,
'show_default': None,
'show_envvar': False,
'type': STRING}
===== outputs of hello() =====
Hello John!
まず、(1) type of param
から、param
の型が Option
だとわかります。
また、Option
は Parameter
のサブクラスであることも確認できました。
次に、(2) vars(param)
から、param
はパラメータの様々なメタデータを保持していることがわかります。たとえば、
'default': 'World'
という部分から、デフォルト値が 'World'
であることがわかります。
value
value
はコマンドラインから渡された値ですが、これも確認しておきましょう。
(実際にはtype
で指定したクラスによって変換された後の値です。)
from pprint import pprint
from typing import Any
import click
def peak_value(
ctx: click.Context,
param: click.Parameter,
value: Any,
):
print("=" * 5, "(1)", "type of value", "=" * 5)
print(type(value))
print()
print("=" * 5, "(2)", "print value", "=" * 5)
pprint(value)
print()
return value
import click
@click.command()
@click.option(
"--count",
default=1,
help="Number of greetings.",
)
@click.option(
"--name",
default="World",
help="The person to greet.",
callback=peak_value,
)
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
print("=" * 5, "outputs of hello()", "=" * 5)
for _ in range(count):
click.echo(f"Hello {name}!")
if __name__ == "__main__":
hello()
name
に値を指定して実行してみます。
$ python what_is_value.py --name John
===== (1) type of value =====
<class 'str'>
===== (2) print value =====
'John'
===== outputs of hello() =====
Hello John!
value
が コマンドラインから渡された値であることが確認できました。
コールバック関数から何を返せばよいか
コールバック関数の返り値は最終的にCLIの関数に渡したい値にすれば大丈夫です。
上の例で言えば、hello
関数のname
に渡したい値を返せばよいです。
4.その他
ここまでコールバック関数について解説してきましたが、実は上で説明したユースケースの中に、コールバック関数以外の方法で実装したほうがよい場合があります。
それに関係する話題を紹介します。
type
を使って入力値の検証と変換を行う
Clickには、入力値の検証と変換を行うための強力なtypeオプションが用意されています。
特にclick.DateTime
を使用することで、日付と時刻の検証と型変換を簡単に行うことができます。
この方法は、以下の理由でコールバック関数を使用するよりも優れていることが多いです。
- 簡潔なコード:
click.DateTime
をtype
に指定するだけで、日付の検証とdatetime
オブジェクトへの変換が自動的に行われます。 - デフォルト値の扱いが容易: 入力値の検証と変換で紹介した例では、値を何も指定しなかった場合にエラーが発生してしまい、それを避けるための追加のコードが必要でした。一方、
click.DateTime
を使用すれば、デフォルト値の設定も含めてより少ないコードで実装できます。 - カスタマイズ性:
click.DateTime
のサブクラスを作成することで、検証ロジックを簡単にカスタマイズできます。こちらのほうがコールバック関数を使用するよりも自然で明確な方法だと思います。
click.DateTime
を使った例を見てみましょう。
import click
@click.command()
@click.option(
"--date",
type=click.DateTime(formats=["%Y-%m-%d"]),
)
def process_date(date):
click.echo(f"処理する日付: {date}")
click.echo(f"dateの型 : {type(date)}")
if __name__ == "__main__":
process_date()
パラメータに値を指定しないで実行してみます。
$ python validate_input_with_type.py
処理する日付: None
dateの型 : <class 'NoneType'>
関数process_date
にNone
が渡されていることがわかります。
パラメータに値を指定したときはどうでしょうか。
$ python validate_input_with_type.py --date 2024-12-31
処理する日付: 2024-12-31 00:00:00
dateの型 : <class 'datetime.datetime'>
$ python validate_input_with_type.py --date 2024/12/31
Usage: validate_input_with_type.py [OPTIONS]
Try 'validate_input_with_type.py --help' for help.
Error: Invalid value for '--date': '2024/12/31' does not match the format '%Y-%m-%d'.
しっかり検証してくれていることがわかりますね。
以上から、単純な検証と型変換はtype
に適切な型を設定するほうが簡単そうだと言えると思います。
次にカスタム検証ロジックを実装してみましょう。
この例では、入力された日付が現在よりも未来であることを確認するカスタム型FutureDateTimeを定義しています。
from datetime import datetime
import click
class FutureDateTime(click.DateTime):
def convert(self, value, param, ctx):
dt = super().convert(value, param, ctx)
if dt <= datetime.now():
self.fail(f"{param.name} must be in the future", param, ctx)
return dt
@click.command()
@click.option("--future-date", type=FutureDateTime(formats=["%Y-%m-%d"]))
def process_future_date(future_date):
click.echo(f"処理する未来の日付: {future_date}")
if __name__ == "__main__":
process_future_date()
実行してみましょう。
$ python validate_custom_logic.py --future-date 2000-01-01
Usage: validate_custom_logic.py [OPTIONS]
Try 'validate_custom_logic.py --help' for help.
Error: Invalid value for '--future-date': future_date must be in the future
$ python validate_custom_logic.py --future-date 2050-01-01
処理する未来の日付: 2050-01-01 00:00:00
うまく検証できていますね。
type
の変換とcallbackはどちらが先に実行されるのか
上の例をみてさらに「type
の変換とcallbackはどちらが先に実行されるんだ?」と疑問に思った人もいると思います。
結論から言うと、 type
の変換 → callback の順で実行されます。
それを確かめましょう。
from typing import Any
import click
class MyParamType(click.ParamType):
name = "my_param_type"
def convert(
self,
value: Any,
param: click.Parameter | None,
ctx: click.Context | None,
) -> Any:
print("From MyParamType!")
return value
def my_callback(
ctx: click.Context,
param: click.Parameter,
value: Any,
):
print("From my_callback!")
return value
@click.command()
@click.option(
"--name",
default="World",
type=MyParamType(),
callback=my_callback,
)
def cli(name):
print(f"Hello {name}!")
if __name__ == "__main__":
cli()
このコードでは、カスタム ParamType
と callback
関数の両方を定義し、それぞれが呼び出されたときにメッセージを出力するようにしています。
実行結果は以下のようになります:
$ python which_comes_first.py
From MyParamType!
From my_callback!
Hello World!
この結果から、以下のことが分かりました。
-
type
による変換(この場合はMyParamType
のconvert
メソッド)が最初に実行されます。 - その後、callback 関数が実行されます。
- 最後に、変換・処理された値が CLI 関数(この場合は
cli
)に渡されます。
このことから、type
による変換とcallbackの使い分けは以下の方針で考えれば良いと思いました。
- type による変換で基本的な型変換や簡単な検証を行い、その結果を callback 関数で利用する
- callback 関数では、既に型変換された値を受け取るため、より複雑な検証や変換を行うことができる
default
に関数を設定する
動的なデフォルト値の設定ではcallback関数を使って動的なデフォルト値を設定しましたが、Clickではdefault
パラメータに関数を設定することで、動的なデフォルト値を簡単に実装できます。
from datetime import date
import click
def get_today():
return date.today().isoformat()
@click.command()
@click.option(
"--date",
default=get_today,
help="処理する日付 (デフォルト: 今日)",
)
def process_date(date):
click.echo(f"処理する日付: {date}")
if __name__ == "__main__":
process_date()
この例では、--date
オプションが指定されなかった場合、get_today
関数が呼び出され、現在の日付がデフォルト値として使用されます。
実行してみましょう。
$ date
Sun Jul 14 08:46:27 UTC 2024
$ python dynamic_default_with_func.py
処理する日付: 2024-07-14
うまくいっていますね。
5.まとめ
この記事では、PythonのClickパッケージを使ってCLIツールにパラメータのコールバックを設定する方法を解説しました。以下の内容をカバーしました。
-
コールバックのユースケース
- 入力値の検証と変換
- 動的なデフォルト値の設定
- 相互依存するパラメータの処理
-
コールバック関数の詳細
- 引数と返り値
- Contextオブジェクト、Parameterオブジェクト、valueの使い方
-
その他の便利な機能
-
type
パラメータを使った入力値の検証と変換 -
default
に関数を設定して動的なデフォルト値を指定する方法
-
これらを学ぶことで、Clickを使ったCLIツールの作成がより柔軟で強力になります。公式ドキュメントも参考にしながら、自分のプロジェクトに適用してみてください。
参考文献
- Click公式ドキュメント: Click Documentation