Python argparse の action API を使ってみる

Pythonで引数を取得するのに便利なargparseモジュール。
add_argumentにactionを指定すると、引数があった時どのように値を格納するかを割り当てられる。
たぶん、よく使われるのは`store_true`を指定して単にフラグを立てるのに使うとか。

```py
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("--hoge", action="store_true")
>>> args = parser.parse_args(["--hoge"])
>>> args.hoge
True
```
で、このactionにAPIを実装したオブジェクトを渡すと任意の下ごしらえができるそうなので使ってみた。

## argparse.Action
[argparseのドキュメント](https://docs.python.org/ja/3/library/argparse.html#action-classes)に従って`argparse.Action`を継承し、`__call__`メソッドを持つクラスを書く。

```py:print_action.py
import argparse

class PrintAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        print "[{}] option add value [{}] as attr [{}]".format(
                option_string, values, self.dest)
        setattr(namespace, self.dest, values)
```
`__call__`メソッドでは次の4つの引数を受け取るようにする。

- `parser` : add_argument する ArgumentParser オブジェクト
- `namespace` : parse_args() で返される namespace オブジェクト
- `values` : 引数の値。add_argumentのtypeが指定されていた場合は先に型変換される。
- `option_string` : オプション引数な場合のオプション文字列。位置引数の場合は渡されない。

上記の`PrintAction`を使ってみる。

```py
>>> import argparse
>>> from print_action import PrintAction
>>>
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("--hoge", action=PrintAction)
>>> args = parser.parse_args("--hoge huga".split(' '))
[--hoge] option add value [huga] as attr [hoge]
>>> args.hoge
'huga'
```

基本的には`values`を適当に加工した上で、引数名(`self.dest`)を属性名に使って`namespace`に追加すればよい。

*追記:* [最近のドキュメント](https://docs.python.jp/3/library/argparse.html#action)には、必要であれば`__init__`もオーバーライドするよう書いてある。`argparse.Action.__init__`では、「2つの位置引数と、action それ自身を除く ArgumentParser.add_argument() に渡されるすべてのキーワード引数を受け付けなければなりません。」と[書いてある](https://docs.python.jp/3/library/argparse.html#action-classes)が、要するに`add_argument`におけるactionの使い方が適切かどうか`__init__`内でチェックできるようだ。

## 例
引数として整数、もしくは`<int>[KMG]`を取り、後者の場合は整数に変換する`action`。

```py:calc_suffix_action.py
import argparse

class CalcSuffixAction(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        """
        省略可。`add_argument`の他の引数がactionにとって適切か判定するならここで
        今回は`__call__`メソッドで受け取る`values`がリストでないことを期待しているので
        `nargs`を制限してみる。
        """
        if nargs is not None and nargs != '?':
            raise ValueError("Invalid `nargs`: multiple arguments not allowed")
        super(CalcSuffixAction, self).__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        try:
            if values.endswith('K'):
                digit = int(values[:-1]) * 1000
            elif values.endswith('M'):
                digit = int(values[:-1]) * 1000000
            elif values.endswith('G'):
                digit = int(values[:-1]) * 1000000000
            elif not isinstance(values, int):
                digit = int(values)
            else:
                digit =	values

        except ValueError:
            parser.error("Invalid argument ({})".format(values))

        setattr(namespace, self.dest, digit)
```

使ってみる。

```py
>>> import argparse
>>> from calc_suffix_action import CalcSuffixAction
>>>
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("--hoge", action=CalcSuffixAction)
>>>
>>> parser.parse_args("--hoge 1024".split(' '))
Namespace(hoge=1024)
>>> parser.parse_args("--hoge 4K".split(' '))
Namespace(hoge=4000)
>>> parser.parse_args("--hoge 32M".split(' '))
Namespace(hoge=32000000)
>>> parser.parse_args("--hoge 1G".split(' '))
Namespace(hoge=1000000000)
>>> parser.parse_args("--hoge 3T".split(' '))
usage: [-h] [--hoge HOGE]
: error: Invalid argument (3T)
```