CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: セットアップスクリプト (setup.py) に自作のコマンドを追加する

インストール可能な Python パッケージを作るには、セットアップスクリプト (setup.py) と呼ばれるファイルを用意する。

セットアップスクリプトは以下のように用いる。 <command> にはパッケージのインストールに使う install やソースコード配布物を生成する sdist などがある。

$ python setup.py <command>

今回はそのセットアップスクリプトに自作のコマンドを追加する方法について書いてみる。 尚、環境には setuptools というパッケージが入っていることを前提にする。 これはサードパーティ製のパッケージだけど、Python のパッケージング用ライブラリとしてはデファクトスタンダードになっているもの。

自作パッケージにコマンドを追加する

まずは自作パッケージに対してコマンドを追加する方法について書いていく。

mysetupcmd という名前でパッケージを作ってみる。 中身は空っぽだけど __init__.py があるため、これでも立派な Python パッケージになる。

$ mkdir mysetupcmd
$ touch mysetupcmd/__init__.py

上記のパッケージに対してセットアップスクリプトを書く。 ポイントは setuptools.set() に対して cmdclass というパラメータを渡しているところ。 ここに setuptools.Command を継承したクラスを渡すことでコマンドが追加できる。

$ cat << EOF > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import Command


class GreetCommand(Command):

    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        print('Hello, World!')


def main():
    setup(
        name='mysetupcmd',
        version='0.0.1',
        zip_safe=False,
        packages=['mysetupcmd'],
        cmdclass={'greet': GreetCommand},
    )


if __name__ == '__main__':
    main()
EOF

これでセットアップスクリプトに greet というコマンドが追加できた。 早速実行してみよう。

$ python setup.py greet
running greet
Hello, World!

上手いこと greet コマンドで GreetCommand#run() の内容が実行された。

追加したコマンドで引数を受け取る

今度は上記のコマンドに引数を取れるようにしてみよう。 user_options に取れるようにしたい引数を設定しておくと、自動的に同名のメンバとしてアクセスできるようになる。 また、initialize_options/finalize_options() メソッドでは引数のデフォルト値などが設定できる。

$ cat << EOF > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import Command


class GreetCommand(Command):

    user_options = [
        ('message=', None, 'output message'),
    ]

    def initialize_options(self):
        self.message = 'Hello, World!'

    def finalize_options(self):
        pass

    def run(self):
        print(self.message)


def main():
    setup(
        name='mysetupcmd',
        version='0.0.1',
        zip_safe=False,
        packages=['mysetupcmd'],
        cmdclass={'greet': GreetCommand},
    )


if __name__ == '__main__':
    main()
EOF

オプションを追加すると --help オプションに詳細が表示されるようになる。 今気づいたけど、ここの表示はクラス名がそのまま出るんだね…。 クラス名はコマンド名と揃えたほうが良さそうかな。

$ python setup.py greet --help
Common commands: (see '--help-commands' for more)

  setup.py build      will build the package underneath 'build/'
  setup.py install    will install the package

Global options:
  --verbose (-v)  run verbosely (default)
  --quiet (-q)    run quietly (turns verbosity off)
  --dry-run (-n)  don't actually do anything
  --help (-h)     show detailed help message
  --no-user-cfg   ignore pydistutils.cfg in your home directory

Options for 'GreetCommand' command:
  --message  output message

usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

これで greet コマンドが --message オプションを受け取れるようになる。

$ python setup.py greet --message='I am a pen!'
running greet
I am a pen!

オプションの指定がない場合は initialize_options() メソッドで指定したデフォルト値のまま。

$ python setup.py greet
running greet
Hello, World!

コマンドをインストールする

さて、これまでのやり方ではパッケージ内でしかコマンドが有効ではなかった。 次は自作のコマンドを Python 実行環境にインストールすることで、どのパッケージのセットアップスクリプトでも使えるようにしてみる。

まずは mysetupcmd パッケージに先ほど作成した GreetCommand クラスを移動する。

$ cat << EOF > mysetupcmd/cmd.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import Command


class GreetCommand(Command):

    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        print('Hello, World!')
EOF

そしてセットアップスクリプトでは entry_points オプションの distutils.commands にコマンド名と上記のクラスを指定する。

$ cat << EOF > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
    setup(
        name='mysetupcmd',
        version='0.0.1',
        zip_safe=False,
        packages=['mysetupcmd'],
        entry_points={
            'distutils.commands': [
                'greet = mysetupcmd.cmd:GreetCommand',
            ],
        }
    )


if __name__ == '__main__':
    main()
EOF

この状態では cmdclass に指定がないため、自身のパッケージでも自作のコマンドは使えない。

$ python setup.py greet
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

error: invalid command 'greet'

では次に、セットアップスクリプトを使って Python 実行環境に mysetupcmd パッケージをインストールしよう。

$ python setup.py install
...(省略)...
Finished processing dependencies for mysetupcmd==0.0.1

これで mysetupcmd パッケージが入った。

$ pip list | grep ^mysetupcmd
mysetupcmd (0.0.1)

すると greet コマンドがセットアップスクリプトで使えるようになる。 もちろん、これはインストールされた mysetupcmd パッケージの効果なので、別のプロジェクトのセットアップスクリプトでも有効になる。

$ python setup.py greet
running greet
Hello, World!

めでたしめでたし。

まとめ

今回はセットアップスクリプトに自作のコマンドを追加する方法について書いた。 紹介した内容はほんのさわりだけど、応用例としてはセットアップスクリプト経由でパッケージのソースコードに対してツールを実行できるようにするといったことが考えられる。