[Python] setuptools

この記事について

この記事は、id:SumiTomohiko:20070623:1182602060の続きです。これが最後の記事になります。

setuptoolsを拡張し、再利用する

distutilsの拡張を作成する

distutilsに新しいコマンドを追加したり、setupの引数を追加するのは困難です。しかし、setuptoolsぱっめーじはこれを少し簡単にします。これは、distutilsの拡張を別のプロジェクトとして配布できるようにし、その拡張を必要としているプロジェクトがsetup_requires引数でそれらを参照するようにするだけで、可能になります。

setuptoolsを使えば、「エントリポイント」を定義するだけで、distutilsの拡張プロジェクトは新しいコマンドとsetup()引数でフックすることができるようになります。コマンドまたは引数名を、ハンドラをインポートする場所の指定に対応づけるマップがあります(エントリポイントの背景について、上の「サービスとプラグインを動的に発見する」の節を参照してください)。

  • コマンドを追加する

distutils.commandsグループにエントリポイントを定義することで、新しいセットアップコマンドを追加できます。例えば、fooコマンドを追加したい場合、distutilsの拡張プロジェクトのセットアップスクリプトに、以下のように記述します:

setup(
    # ...
    entry_points = {
        "distutils.commands": [
            "foo = mypackage.some_module:foo",
        ],
    },
)

(もちろん、mypackage.some_moduleにあるfooクラスはsetuptools.Commandクラスの派生クラスであることを前提としています)

このようなエントリポイントを持つプロジェクトがいったんsys.path上で実行されたら(すなわち、site-packagesインストールディレクトリで"install"か"develop"した場合)、このコマンドはsetuptoolsに基づいたどのセットアップスクリプトでも有効になります。--command-packagesオプションを使用する必要はなく、新しいコマンドをインストールするためにdistutils.commandパッケージにパッチをあてる必要もありません; setuptoolsは自動的にdistutilsにラッパーを追加し、sys.path上の有効になっている配布物でエントリポイントを探せるようにします。実際、これこそがsetuptoolsが自分のコマンドをインストールする方法なのです: setuptoolsのプロジェクトのセットアップスクリプトはコマンドのエントリポイントを定義しているのです!

  • setup()の引数を追加する

ときには、新しいコマンドはsetup()の呼び出しに新しい引数を必要とするでしょう。これは、distutils.setup_keywordsグループにエントリポイントを定義することで可能になります。例えば、bar_bazというsetup()の引数を必要とした場合、distutilsの拡張プロジェクトのセットアップスクリプトに以下を追加することができます:

setup(
    # ...
    entry_points = {
        "distutils.commands": [
            "foo = mypackage.some_module:foo",
        ],
        "distutils.setup_keywords": [
            "bar_baz = mypackage.some_module:validate_bar_baz",
        ],
    },
)

ここでの考え方は、与えられた場合、setup()の引数の妥当性を検証するために呼び出される関数を、エントリポイントが定義する、ということです。Distributionオブジェクトは初期値がNoneである属性を持っており、setup()の呼び出しがNone以外の値を設定していたときだけ、値を検証する関数が呼び出されます。以下は、値を検証する関数の例です:

def assert_bool(dist, attr, value):
    """値がTrueかFalse, 0, 1であることを検証します"""
    if bool(value) != value:
        raise DistutilsSetupError(
            "%r は真偽値でなければなりません(実際は%rでした)" % (attr,value)
        )

関数は、3つの引数を取ります: Distributionオブジェクトと属性の名前、属性の値です。引数が不正な場合は、DistutilsSetupError(distutils.errorモジュール)を生成しなければなりません。関数はNone以外の値のときだけ呼び出され、この方法で定義された引数のデフォルトの値はNoneであることを、覚えていてください。このため、あとで属性にアクセスしたとき、その値がNoneである可能性があることに、新しいコマンドは常に気をつけていなければなりません。

もしふたつ以上の有効な配布物が同じsetup()の引数のエントリポイントを定義したら、それらすべてが呼び出されます。これにより、引数のどんな値が妥当か合意している限り、複数のdistutilsの拡張が共通の引数を定義することができるようになります。

コマンドにおいては、新しい引数を追加するために、distutilsのDistributionクラスを派生したり、パッチをあてたりするひつようがないことにも、注意してください; あなたの拡張を使用するどのセットアップスクリプトもsetup_requires引数であなたのプロジェクトを記述している限り、これは拡張の中でエントリポイントを定義するとき重要です。

  • 新しいEGG-INFOファイルを追加する

拡張可能なアプリケーションやフレームワークは、pkg_resourcesメタデータAPIを通して簡単にアクセスできるように、プラグインのEGG-INFOディレクトリに含まれるアプリケーションかフレームワーク固有のメタデータと一緒に、サードパーティがプラグインを開発できるようにしたいと思うでしょう。これを可能にするもっとも簡単な方法は、(setup_requiresを通して)新しいセットアップキーワードを定義し、egg_infoコマンドが実行されたときにEGG-INFOファイルにそのメタデータを書き込む、プラグインのプロジェクトのセットアップスクリプトから使用されるdistutilsの拡張を作成することです。

egg_infoコマンドはegg_info.writersグループで拡張ポイントを探し、ファイルに書き込むためにそれらを呼び出します。ここに、setupの引数foo_barを定義する、distutilsの拡張の例があります。foo_barは、この引数を使用するすべてのプロジェクトのEGG-INFOディレクトリのfoo_bar.txtに書き込まれる行のリストです。

setup(
    # ...
    entry_points = {
        "distutils.setup_keywords": [
            "foo_bar = setuptools.dist:assert_string_list",
        ],
        "egg_info.writers": [
            "foo_bar.txt = setuptools.command.egg_info:write_arg",
        ],
    },
)

この例では、setuptoolsが自分で使うために定義したふたつのユーティリティ関数が使われています: セットアップのキーワードが文字のシーケンスであることを検証するルーチンと、セットアップの引数を探し、それをファイルに書き込むルーチンです。この書き込むユーティリティは、以下のようになっています:

def write_arg(cmd, basename, filename):
    argname = os.path.splitext(basename)[0]
    value = getattr(cmd.distribution, argname, None)
    if value is not None:
        value = '\n'.join(value)+'\n'
    cmd.write_or_delete_file(argname, filename, value)

見てのとおり、egg_info.writersのエントリポイントは3つの引数をとる関数でなければなりません: egg_infoコマンドのインスタンスと書き込むファイルの名前(すなわちfoo_bar.txt)、書き込むファイルの完全な名前です。

一般的には、書き込む関数はファイルに書き込む際、コマンドオブジェクトのdry_runの設定を尊重し、コンソールの出力にはdistutils.logオブジェクトを使用します。この要求に沿うもっとも簡単な方法は、ファイルを操作するときに、cmdオブジェクトのwrite_file()メソッドとdelete_file()メソッド、write_or_delete_file()メソッドを排他的に使用することです。詳細は、各メソッドのドキュメント文字列を参照してください。

もしCVSSubversion以外のバージョン管理システムからファイルを探すsetuptoolsのプラグインをつくろうとしていたら、setuptools.file_findersグループにエントリポイントを追加してできます。エントリポイントは関数であり、ひとつのディレクトリ名を受け取り、そのディレクトリ(とすべてのサブディレクトリ)の中でバージョン管理システムによって管理されているすべてのファイル名を返します。

例えば、"foobar"と呼ばれるバージョン管理システムに対応するプラグインをつくろうとしたら、この関数を以下のようにして記述します:

def find_files_for_foobar(dirname):
    # `dirname`で始まるパスを繰り返して返します

そしてこれを以下のようにしてセットアップスクリプトに登録します:

entry_points = {
    "setuptools.file_finders": [
        "foobar = my_foobar_module:find_files_for_foobar"
    ]
}

それから、あなたのプラグインを使いたいと思った人は誰でも、単にこれをインストールするだけで、ローカルのsetuptoolsは必要なファイルを探せるようになります。

単に他のバージョン管理システムを使用するプロジェクトと一緒に、バージョン管理システムのプラグインを配布する必要はなく、setup_requiresにそのプラグインを指定する必要はありません。sdistコマンドでソース配布物を作成したら、setuptoolsは自動的にSOURCES.txtファイルにどのファイルが見つかったか書き込みます。これにより、ソースの配布物を受け取る人たちは、バージョン管理システムをまったく必要としません。しかし、誰かがそのシステムでチェックアウトしたパッケージで作業をしていたら、彼らはもともとの作者が使っているのと同じプラグインを必要とするでしょう。

バージョン管理システムのファイルを検索する関数を記述する際に、重畳な点がいくつかあります:

  • あなたの関数は渡されたディレクトリ名を付加して、相対パスを返さなければなりません絶対パスと、渡されたディレクトリの親ディレクトリを参照する相対パス許されていません
  • あなたの関数は、ディレクトリ名として、カレントディレクトリを意味する空文字列を受け付けなければなりません。これをドットに変換してはなりません; 相対パスだけを返します。このため、カレントディレクトリの下にあるsome/dirと名付けられたサブディレクトリを返すときは、./some/dirや/somewhere/some/dirと変換してはならず、常に単純にsome/dirとします。
  • あなたの関数はいかなるエラーも生成してはなりませんし、必要なプログラム(すなわち、バージョン管理システムそのものに属するプログラム)がないときも行儀よく動作しなければなりません。しかし、distutils.log.warn()を使用して、ユーザにプログラムがないことを教えることはできます。
  • コマンドを継承する

ごめんなさい、この節はまだ書いていません。更新履歴を除く、これより下の多くの節もそうです。新しいドキュメントが追加されるか更新されたとき、このページを見たいと思うでしょう。

setuptoolsのコードを再利用する
  • ez_setup

XXX

  • setuptools.archive_util

XXX

  • setuptools.sandbox

XXX

  • setuptools.package_index

XXX

リリースノート/変更履歴

省略。

メーリングリスト

setuptoolsについての質問や議論は、distutils-sig mailing listでどうぞ。