distributeのextra_commandの作り方

pythonのsetup.pyを使って以下のように実行可能なコマンドを作ってみる

$ python setup.py <command name> <args> ...

(作業した内容のgist)

setup.py

pythonのパッケージはsetup.pyを持っている。

このsetup.pyは提供されるパッケージに関するコマンドを持っている。

例えば、以下のような機能がある。

  • packageをpypiにuploadしたり
  • テストランナーを走らせたり
  • 配布用のバイナリを作ったり
extra_command

pythonのsetup.pyが提供するコマンドを自分で作成することができる。
利用できるコマンドは以下のようにすると分かる。

# $ ls | grep setup.py
# setup.py

$ python setup.py --help-command
# Standard commands:
# ...
# 
# Extra commands:
#   rotate            delete older distributions, keeping N newest files
#   develop           install package in 'development mode'
#   setopt            set an option in setup.cfg or another config file
#   saveopts          save supplied options to setup.cfg or other config file
#   egg_info          create a distribution's .egg-info directory
#   nosetests         Run unit tests using nosetests
#   alias             define a shortcut to invoke one or more commands
#   easy_install      Find/get/install Python packages
#   bdist_egg         create an "egg" distribution
#   install_egg_info  Install an .egg-info directory for the package
#   test              run unit tests after in-place build
# 
# usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
# ...

rotateとかdevelopとか定義されている。

自分でコマンドを追加してみよう

行う手順は以下のとおり

  • entry_pointに追加する
  • 呼ばれるコマンドを作成
entry_pointに追加する。

setup.pynのetnry_pointに追加する。
今回のディレクトリ構成は以下のとおり。foo/scripts.pyに追加する予定

.
├── foo
│   ├── __init__.py
│   └── scripts.py ## ここにfoo_commandを作成
│
└── setup.py

setup.pyにentry_pointsを追加.(scripts.pyのfoo_command)

setup(
    # ...
    entry_points = {
        "distutils.commands": [
            "foo = foo.scripts:foo_command",
        ],
    },
)
commandを作成する

(試しに関数を書いてみる)

# scripts.py
def foo_command(*args, **kwargs):
    pass

このパッケージをインストールしたときfooコマンドが使えるようになれば良い

$ python setup.py develop
$ python setup.py --help-commands | grep foo
#  foo               (no description available)

## あった。実行してみる
$ python setup.py foo
# ..
# TypeError: issubclass() arg 1 must be a class

実行できたけど、エラーで落ちた。次は真面目に実装してみる

真面目にコマンドを実装

調べてみると、distributeのextra_commandsは以下の要件を満たしている必要があるらしい。

  • setuptools.Commandのsubclass
  • run,initialize_options,finalize_optionsを実装
  • その他細かな事
    • user_optionsでオプショナル引数の指定
      • (末尾が=のものは引数をとる)
      • command_consumes_arguments=Trueにすると引数を受け取れるようになる。
from setuptools import Command

class foo_command(Command):
    description = "echo foo"
    
    ## user_option = [(long, short, help-message),  ....]
    # e.g. 
    #  long  :: "verbsoe"
    #  short :: "v"
    #  help  :: "verbose output"
    #
    user_options = [("user=", "u", "echo user")]
    command_consumes_arguments = True

    def run(self):
        print self.args
        print "user: %s foo" % self.user

    def initialize_options(self):
        self.args = []
        self.user = None

    def finalize_options(self):
        pass

実行可能になった。

$ python setup.py foo -u xxx 1 2 3 4
# running foo
# ['1', '2', '3', '4']
# user: xxx foo
 
$ python setup.py foo 1 2 3 4
# running foo
# ['1', '2', '3', '4']
# user: None foo

appendix コマンドにaliasを付ける。(setup.cfgでデフォルトの設定を付加する)

ついでに、setup.cfgを書いてdefaultの設定を付加してみることにする。
ここでは、上で作ったfooコマンドの-uの値を"User"に設定してみる。

setup.pyで結びつけたコマンド名をセクションにして設定を記述する。

#setup.cfg
[foo]
user = "User"

こうすると、python setup.py fooを実行したとき、デフォルトで-u "user"が渡された状態になる。

$ python setup.py foo 1
running foo
['1']
user: "User" foo