CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: コマンドラインパーサの Click で独自の型を使う

これは Python Advent Calendar 2015 の 2 日目の記事です。

サードパーティ製のコマンドラインパーサ Click がとても便利で標準ライブラリの argparse を使っている場合じゃない件については以前このブログで書きました。 blog.amedama.jp

今回は、その記事では紹介できなかった、独自の型を定義して Click で使う方法について書いてみます。

インストール

まずは何はともあれ Click をインストールしておきましょう。

$ pip install Click
Collecting Click
  Downloading click-6.2-py2.py3-none-any.whl (70kB)
    100% |████████████████████████████████| 73kB 133kB/s 
Installing collected packages: Click
Successfully installed Click-6.2

Click の型って?

ここで Click の型と呼んでいるのは click.option() や click.argument() で指定できる引数 type に指定できる値のことです。 例えば click.option() の引数 type に組み込み型の int を指定した場合のサンプルコードを以下に示します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--integer', '-I', type=int)
def main(integer):
    click.echo(integer)
    click.echo(type(integer))


if __name__ == '__main__':
    main()

上記のオプション --integer/-I には int として解釈できる値が渡されると、それが int 型にキャストされた状態で引数 integer にインジェクトされます。 そして、入力された内容が int として解釈できない場合にはコマンドの実行がエラーとなります。

$ python typeint.py --integer 100
100
<type 'int'>
$ python typeint.py --integer foo
Usage: typeint.py [OPTIONS]

Error: Invalid value for "--integer" / "-I": foo is not a valid integer

独自の型を追加する

先ほど例では int があらかじめ Click で使えるようになっていましたが、これと同じ考え方で独自の型が使えると便利そうです。 今回は試しに IPv4/IPv6 アドレスを受け取ることができる独自の型を定義してみます。

Python 3.3+ であれば Python に IP アドレスを扱うための標準ライブラリ ipaddress が用意されているのでこれを使うことにします。 もし Python 2.x を使う場合には ipaddress のバックポートである py2-ipaddress があるので、別途インストールしてください。

$ pip install py2-ipaddress  # Python 2.x の場合

早速サンプルコードです。 独自の型を作るには click.ParamType を継承したクラスを定義して convert() メソッドをオーバーライドします。 引数の value がユーザの入力した値に対応するので、これを元に IP アドレスのオブジェクトに変換してメソッドの返り値にします。 もし、変換できない (適切な IP アドレスではない) 値が入力された場合には click.ParamType#fail() メソッドでそれを通知します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import ipaddress

import click


class IPAddressType(click.ParamType):
    name = 'ipaddress'

    def convert(self, value, param, ctx):
        try:
            return ipaddress.ip_address(value)
        except ValueError:
            message = '{value} is not a valid ip address'.format(value=value)
            self.fail(message, param, ctx)


IP_ADDRESS = IPAddressType()


@click.command()
@click.option('--ip-address', '-I', type=IP_ADDRESS)
def main(ip_address):
    click.echo(ip_address)
    click.echo(type(ip_address))


if __name__ == '__main__':
    main()

それでは、上記を実行してみましょう。

$ python typeip.py --ip-address 192.168.2.1
192.168.2.1
<class 'ipaddress.IPv4Address'>
$ python typeip.py --ip-address 2001:db8::1
2001:db8::1
<class 'ipaddress.IPv6Address'>

IPv4/IPv6 アドレスとして解釈できる値が入力されると、それが適切なオブジェクトとなって引数にインジェクトされています。

もちろん、IP アドレスとして解釈できない値が入力されるとエラーになります。

$ python typeip.py --ip-address 123.456.789.123
Usage: typeip.py [OPTIONS]

Error: Invalid value for "--ip-address" / "-I": 123.456.789.123 is not a valid ip address

ばっちりですね。