optparse のコールバック処理を argparse モジュールへ移行する

Pythonコマンドライン引数の解析ツールは getoptoptparse、さらに Python 2.7 から argparse が標準ライブラリとして追加されています。

Python 2.6 環境では argparse 使えないからと、私は argparse の使用を少し躊躇していたところがあったのですが、PEP389(和訳) によると、argparse は Python 2.3 以上で使えるようなので、optparse がなければ argparse 1.2.1 : Python Package Index からインストールすれば良いんだと気付きました。

最近、車輪の再発明をしている(?)記事 *1 を見つけたりと、今から開発するコマンドライン引数を受け取るプログラムには、もっと積極的に argparse を使おうよと呼びかけてみようと思った次第なんです。argparse の使い方は、argparse – コマンドラインオプションと引数の解析argparseを使ってみた - そこはかとなく書くよ。 を参考にしてください。

argparse に限っては、よく分からなくても新しい方のライブラリ使っておいたら良いよ的に考えても大丈夫だと思います。

そこで過去に自分が実装した処理を optparse から argparse へ移行していたら、optparse でコールバック関数を定義していた処理が、argparse ではカスタムアクションを定義するように変わっていることに気付きました。コールバック関数のちょっとした移行 Tips として簡単にまとめておきます。その他の移行の注意点は optparse のコードをアップグレードする を参照してください。

まずは optparse で action にコールバック関数を呼び出す処理です。

def get_lower(option, opt_strings, values, parser):
    parser.values.lower = values.lower()

def get_optparse_args():
    """
    >>> import sys
    >>> sys.argv = ["sample.py", "-l", "TEST"]
    >>> get_optparse_args()
    ('Values', "{'lower': 'test'}")
    """
    import optparse
    parser = optparse.OptionParser()
    parser.add_option("-l", "--lower", dest="lower", type="string",
                      action="callback", callback=get_lower,
                      metavar="LOWER", help="callback test")
    opts, args = parser.parse_args()
    return opts.__class__.__name__, str(opts)

"-l" で受け取る引数の文字列を小文字に変換して格納するだけの処理です。この処理を argparse で実装する場合は、次のようにカスタムアクションを定義します。

import argparse

class LowerAction(argparse.Action):
    def __call__(self, parser, namespace, values, options_string=None):
        setattr(namespace, self.dest, values.lower())

def get_argparse_args():
    """
    >>> import sys
    >>> sys.argv = ["sample.py", "-l", "TEST"]
    >>> get_argparse_args()
    Namespace(lower='test')
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("-l", "--lower", dest="lower",
                        action=LowerAction,
                        metavar="LOWER", help="callback test")
    opts = parser.parse_args()
    return opts

argparse.Action を継承して __call__ をオーバーライドします *2 。こういうシンプルな用途だと、わざわざクラスを作るのも面倒な気がしますが、より柔軟性のあるカスタムアクションが定義できるようになったんだろうと思います。

optparse で使用したコールバック関数 get_lower() と argparse で使用した LowerAction の簡単なテストコードは、それぞれ次のようになります。

# -*- coding: utf-8 -*-
from nose.tools import *
from migrate_optparse_to_argparse import *

def test_get_lower():
    class Parser(object):
        class Values(object):
            lower = None
        values = Values()
    parser = Parser()
    get_lower(None, None, "TEST", parser)
    assert_equal("test", parser.values.lower)

def test_lower_action():
    class Namespace(object):
        lower = None
    namespace = Namespace()
    a = LowerAction(["-l", "--lower"], dest="lower")
    a(None, namespace, "TEST", None)
    assert_equal("test", namespace.lower)