2020-10-09

pythonのargparseモジュールでコマンドラインで指定した文字列を数値に変換するロジック

Pythonを利用して簡単なツールを作成しているところです。ツールを組むにあたり、Pythonが標準的に提供しているモジュールを活用することを基本とし、同じようなロジックを自前で作らないようにしたいと考えています。この方針のもとに、コマンドライン引数を解釈するのはモジュール「argparse」 を、ログ出力にはモジュール「logging」を使用しようと思っています。


モジュール「logging」では、ログレベルの表記と数値が「16.6.2. ロギングレベル」で定められています。例えば「DEBUG」なら「10」、「WARNING」なら「30」のようになります。このようなログレベルをコマンドライン引数で指定できるようにしたいのですが、考えておく必要のある事項があります。

  1. コマンドラインでは文字列(「DEBUG」など)を指定したとしても、ツール内部では対応する数値(「DEBUG」が指定されたのであれば「10」)で処理したい。
  2. どの文字列がどの数値に対応しているのかという「知識」をユーザが持つ必要が無いようにしたい。

モジュール「argparse」ではメソッド「add_argument()」において「choices」を使えば、考えているような事ができると思います。しかし注意しておかなければのは、選択肢を「choices={'debug', 'warning', ...}」のように定義すれば、考えているような形でコマンドライン引数を指定することができます。ただしモジュール「argparse」のメソッド「parse_args()」の結果としては、変数に格納される値も「文字列」になってしまいます。これを対応する数値にする方法はないか考えてみました。

当初はメソッド「add_argument()」の仕組みの中で対処しようと考えていました。例えば「choices」に指定しておくのは対応する数値の方にしておき、「type=lambda s: myfunc(s)」のようにしておけば、自前の関数「myfunc()」の中で「コマンドラインで与えられた文字列を、対応する数値に変換」することができるようになります。これで意図した動作をするのですが、もしコマンドラインでオプション「-h」を指定すると表示されるヘルプメッセージでは、選択肢として「数値の列」が出てきてしまいます。この問題は「metavar」を使えば対処できます。

これで問題が全て解決したかと思いましたが、まだ問題が残っていました。もし選択肢にない文字列をコマンドラインで指定した場合、エラーメッセージ「invalid choices」が表示され、そこでは正しい選択肢として「choices=」で指定している情報が出力されます。それは「数値」であり「文字列」にはなっていません。モジュール「argparse」の実装(/usr/lib64/python3.6/argparse.py)を確認すると、以下のようになっていました。この問題はモジュールの実装に手を入れない限り解決できないようです。

def _check_value(self, action, value):
# converted value must be one of the choices (if specified)
if action.choices is not None and value not in action.choices:
args = {'value': value,
'choices': ', '.join(map(repr, action.choices))}
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
raise ArgumentError(action, msg % args)


モジュール「argparse」の仕組みの中では問題を解決することはできませんので、モジュールの外で対処するしかなさそうです。その結果として以下のようなロジックにしました。これならば意図していたような動作にはなりそうです。

#!/usr/bin/python3

import argparse
import logging

def parseArgv():
        loglevel = {'debug':10,'info':20,'warning':30,'error':40,'critical':50}
        parser = argparse.ArgumentParser()
        parser.add_argument('-l', '--level',
                choices=loglevel.keys(),
                default='info',
                type=str.lower,
                help='log level not case sensitive (default: %(default)s)')
        parser.add_argument('-v', '--version',
                action='version',
                version='%(prog)s 1.0')
        args = parser.parse_args()
        args.level = loglevel[args.level]
        return args

def outputLog():
        # create logger
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.DEBUG)

        # create console handler
        handler = logging.StreamHandler()
        handler.setLevel(logging.DEBUG)

        # create formatter
        formatter = logging.Formatter(
                '%(asctime)s %(name)s:%(levelname)s:%(message)s',
                datefmt='%Y/%m/%d-%H:%M:%S')

        # add formatter to handler
        handler.setFormatter(formatter)

        # add handler to logger
        logger.addHandler(handler)

        # application code
        logger.setLevel(args.level)
        logger.warning('Watch out!')
        logger.info('I told you so')
        logger.debug('often makes a very good meal of %s', 'visiting tourists')

if __name__ == "__main__":
        args = parseArgv()
        outputLog()
#[EOF]

0 件のコメント:

コメントを投稿