タスクの定義

Fabric 1.1からは、fabfileの中でどのオブジェクトをタスクとして示すかを定義するために利用できる、2つのはっきりと異なった方法があります:

  • 1.1からスタートした "新しい" 方法は Task もしくはそのサブクラスのインスタンスを考慮し、また、ネストされた名前空間を構築できるようにするためにインポートされたモジュールに落とし込まれています。
  • "クラシックな" 方法は1.0以前からのもので、すべてのパブリックな呼び出し可能なオブジェクト(関数やクラスなど)を考慮し、インポートされたモジュールへの再帰的には処理せず、そのfabfileのオブジェクトのみ考慮します。

注釈

これらの2つの方法は 相互に排他的です : もしFabricがfabfile内やインポートしたモジュール内に どんな 新しいスタイルのタスクオブジェクトでも見つければ、タスク宣言のこのメッソドをコミットしたとしてみなされ、Task 以外の呼び出しは考慮されません。新しいスタイルのタスクが なければ クラシックな挙動に戻ります。

このドキュメントではこの2つのメソッドを詳細に説明します。

注釈

fabfileでどのタスクが fab 経由で実行されうるかを厳密に確認するには fab --list を使います。

新しいスタイルのタスク

Fabric 1.1では新しい機能を促進し、プログラミングのベストプラクティスを可能にするために Task クラスを導入しました。特に:

  • オブジェクト指向のタスク。継承とそれに伴うすべては、単純に関数オブジェクトを渡し回るより、より実用的なコードの最利用を可能のします。タスク宣言のクラシックなスタイルは完全に排除されているわけではありませんでしたが、ことをとても簡単にするわけでもありませんでした。
  • 名前空間。タスクの宣言を明確なメソッドにすることによって、例えば、(クラシックな手順のもとで有効な "タスク" として表示される)Python os モジュールのコンテンツでタスクリストを汚すことなく、再帰的な名前空間のセットアップが容易になります。

Task の前置きとして、新しいタスクをセットアップするには2つの方法があります:

  • @task で通常のモジュールレベルの関数をデコレートします。これは Task サブクラス内の関数を透過的にラップします。実行時には関数名がタスク名として使われます。
  • Task サブクラス(Task 自身は抽象的であることを意図されています)は run メソッドを定義し、モジュールレベルであなたのサブクラスをインスタンス化します。インスタンスの name 属性がタスク名として使われます。省略した場合には、その代わりにインスタンスの変数名が使われます。

新しいスタイルのタスクの利用はまた、 名前空間 のセットアップも可能にします。

@task デコレータ

新しいスタイルのタスク機能を利用するもっとも簡単な方法は基本のタスク関数を @task: で囲ってしまう方法です:

from fabric.api import task, run

@task
def mytask():
    run("a command")

このデコレータが使われると、このデコレータで囲われた関数 のみ が有効なタスクとして読み込まれることをFabricに伝えます。(表示されない場合は、 クラシックスタイルの 挙動で動作しています)

引数

@task はまた、引数とともに呼び出してその挙動をカスタマイズすることもできます。以下に記述されていない引数は利用中の task_class のコンストラクタにその最初の関数自身を最初の引数として渡されます。(詳細は @task とのカスタムサブクラスの使用 をご覧ください)

  • task_class: Task のサブクラスで、デコレートされた関数をラップするために使用されます。デフォルトは WrappedCallableTask です。
  • aliases: ラップされた関数用のエイリアスとして繰り返し使える文字列名。詳細は エイリアス をご覧ください。
  • alias: aliases と似ていますが、繰り返しできない単一の文字列引数を取ります。もし aliasaliases の両方が設定されている場合、 aliases の方が優先されます。
  • default: デコレートされたタスクが、タスク名としてそれが含むモジュールの代役を務めるかどうかを決めるための真偽値。デフォルトのタスク を参照してください。
  • name: そのタスクがコマンドラインインターフェースに表示されるときの名称を設定する文字列です。Pythonのビルトインを別の方法でシャドーイングするタスク名で便利です(これは技術的には可能ですが、好まれませんし、バグの温床にもなります)。

エイリアス

以下は、人間が読める長めのタスク名とすばやくタイプするための短めのタスク名の両方の利用を手助けするための alias キーワード引数の簡単な利用例です:

from fabric.api import task

@task(alias='dwm')
def deploy_with_migrations():
    pass

このfabfileで --list を呼び出すとオリジナルの``deploy_with_migrations`` とエイリアスの dwm: を表示します:

$ fab --list
Available commands:

    deploy_with_migrations
    dwm

同じ関数に複数のエイリアスが必要な場合は、単に aliases キーワード引数をスワップします。これにより、単一の文字列の代わりに繰り返し利用可能な文字列が取られます。

デフォルトのタスク

aliases と同じような方法で、モジュール内の与えられたタスクを "default" タスクとして指定するときに便利な場合があり、そのモジュール名を 単に 言及することで呼び出すこともできます。これによりタイピングが省略できたり、一つの "メイン" タスクとたくさんの関連タスクもしくはサブモジュールがある場合のより整理された構成が可能になります。

例えば、 deploy サブモジュールが新しいサーバのプロビジョニング、コードのプッシュ、データベースの移行などのタスクを含んでいるとして、デフォルトの "just deploy" アクションとしてタスクを強調できるととても便利でしょう。そうした deploy.py モジュールは次のようになります:

from fabric.api import task

@task
def migrate():
    pass

@task
def push():
    pass

@task
def provision():
    pass

@task
def full_deploy():
    if not provisioned:
        provision()
    push()
    migrate()

タスクリストは以下のようになります (単に deploy を読み込んでいるだけの簡単なトップレベルの fabfile.py であると仮定します):

$ fab --list
Available commands:

    deploy.full_deploy
    deploy.migrate
    deploy.provision
    deploy.push

デプロイのたびに deploy.full_deploy を呼び出すのはちょっと古めかしいですし、チームに加わった新らしい方にとってはこれが実行する正しいタスクなのか迷うことでしょう。

@task への default キーワード引数を利用することにより、例えば、デフォルトのタスクとして full_deploy をタグ付けすることができます:

@task(default=True)
def full_deploy():
    pass

このようにアップデートするとタスクリストは以下のようになります:

$ fab --list
Available commands:

    deploy
    deploy.full_deploy
    deploy.migrate
    deploy.provision
    deploy.push

full_deploy は明示的なタスクとしてそのままあることに留意してください。そして、 full_deploy のある種トップレベルのエイリアスとして deploy が表示されます。

もし一つのモジュール内に複数の default=True がセットされている場合は、最後に読み込まれたもの(通常はファイルの最も下にあるもの)が優先されます。

トップレベルのデフォルトタスク

トップレベルのfabfileで @task(default=True) を使用すると、ユーザーがタスク名なしで fab を呼び出した際にそのタスクを実行します(例えば、 make に似ています)。このショートカットの使用時にはタスク自身に引数を指定することはできません。引数が必要な場合は通常のタスク呼び出しを実行してください。

Task サブクラス

クラシックスタイルのタスク に慣れている場合は、 Task はその run メソッドがクラシックなタスクとそのまま同等であると考えると分かりやすいでしょう。その引数が(self 以外の)タスクの引数で、そのボディが実行される内容となります。

例えば、この新しいスタイルのタスクは:

class MyTask(Task):
    name = "deploy"
    def run(self, environment, domain="whatever.com"):
        run("git clone foo")
        sudo("service apache2 restart")

instance = MyTask()

この関数ベースのタスクとまったく同等です:

@task
def deploy(environment, domain="whatever.com"):
    run("git clone foo")
    sudo("service apache2 restart")

クラスのインスタンスをどのように作成しているかに留意してくだだい。これは実際に動作する単純な通常のPythonオブジェクト指向プログラミングです。この時点では小さなひな形に過ぎませんが、例えば、Fabricはインスタンス作成時に与える名前は気にせず、そのインスタンスの 名前 属性のみを気にします。クラスの力(ちから)を利用可能にすることの恩恵は検討に値するでしょう。

将来的にはこのAPIを拡張して、このエクスペリエンスをより洗練させていく予定です。

@task とのカスタムサブクラスの使用

カスタムな Task サブクラスと @task を結合させることも可能です。これはコアの実行ロジックがクラス/オブジェクト指向ではないけれどもクラスのメタプログラミングやそれと似たテクニックを活用したい場合に有用です。

特に、呼び出し可能なものとして最初のコンストラクタ引数を取るように設計されているすべての Task サブクラスは(ビルトインの WrappedCallableTask と同様に)、 @task への task_class 引数として指定することができます。

Fabricは与えられたクラスのコピーを自動的にインスタンス化し、最初の引数としてラップされた関数に渡されます。デコレーターに渡されるすべての他の引数/キーワード引数( 引数 に記述されている "特別な" 引数以外)はその後に追加されます。

これを明確にするための簡単でいくらか工夫されている例をお見せします:

from fabric.api import task
from fabric.tasks import Task

class CustomTask(Task):
    def __init__(self, func, myarg, *args, **kwargs):
        super(CustomTask, self).__init__(*args, **kwargs)
        self.func = func
        self.myarg = myarg

    def run(self, *args, **kwargs):
        return self.func(*args, **kwargs)

@task(task_class=CustomTask, myarg='value', alias='at')
def actual_task():
    pass

このfabfileが読み込まれた時、CustomTask のコピーがインスタンス化され、事実上は以下を呼び出しています:

task_obj = CustomTask(actual_task, myarg='value')

alias キーワード引数がデコレーター自身によってどのように取り除かれるか、そしてクラスのインスタンス化には到達しないことに留意してください。これは コマンドラインタスクの引数 の動作と機能的に同一です。

名前空間

クラシックなタスク では、複数のfabfileは単一でフラットなタスク名のセットに制限され、それらを体系化する本格的な方法はありません。Fabric 1.1以降では、タスクを新しいやり方(@task もしくは ご自分の Task サブクラスのインスタンス経由)で宣言すれば 名前空間 を活用することができます:

  • fabfileにインポートされたどのモジュールオブジェクトも再帰的に処理され、追加のタスクオブジェクトを探します。
  • サブモジュール内では、Python標準の __all__ モジュールレベル変数名を使ってどのオブジェクトが "exported" されるかをコントロールすることができます(有効な新しいタスクオブジェクトでなければなりませんが)。
  • これらのタスクは、Python自身のインポートシンタックスと似た、それを含んだモジュールをベースにした新しいドットノーテーション名が与えられます。

単純な複雑なものまでfabfileのパッケージを組み立てて、どのように動作するか見てみましょう。

基本

まずはいくつかのタスクを含む一つの __init__.py (簡潔性のためFabricのAPIは省略します)から始めてみましょう:

@task
def deploy():
    ...

@task
def compress():
    ...

fab --list の出力は次のようになるでしょう:

deploy
compress

ここでは名前空間は一つだけで、 "root" もしくはグローバルな名前空間です。今は単純に見えますが、実世界のfabfileではたくさんのタスクがあり、管理が難しくなり得ます。

サブモジュールのインポート

前述のとおり、モジュールがPythonのインポートパス上のどこにあるかには関係なく、Fabricはインポートされたモジュールオブジェクトのタスクを調べます。今のところは、とりあえず "手近にある" 自前のタスクを含めたいと思いますので、そうですね、ロードバランサを扱うためのパッケージに新しいサブモジュール lb.py を作ってみましょう:

@task
def add_backend():
    ...

そして __init__.py の一番上にこれを追加します:

import lb

さて、これで fab --list は次のようになります:

deploy
compress
lb.add_backend

モジュールに一つだけのタスクではある種他愛のないもののように見えますが、その恩恵はかなり明白だと思います。

さらに奥深くへ

名前空間化は単にひとつのレベルに制限されることはありません。より大きなセットアップを持ち、データベース関連のタスクのための名前空間が必要になっていて、その中に追加の別のタスクがあるとしましょう。 db/ と名前を付けられたサブパッケージを作り、その中に migrations.py モジュールを起きます:

@task
def list():
    ...

@task
def run():
    ...

このモジュールは db をインポートしているすべてから見えるようにする必要があるので、このサブパッケージの __init__.py に以下を追加します:

import migrations

最後のステップとして、ルートレベルの __init__.py にサブパッケージをインポートします。これで最初の何行かは以下のようになります:

import lb
import db

そして、ファイルツリーは以下のようになります:

.
├── __init__.py
├── db
│   ├── __init__.py
│   └── migrations.py
└── lb.py

fab --list は次のようになります:

deploy
compress
lb.add_backend
db.migrations.list
db.migrations.run

また、タスクを db/__init__.py 内に直接設定(もしくはインポート)することも可能で、ご想像どおり db.<なんちゃら> として表示されます。

__all__ での制限

インポートされたモジュールをFabricが分析するときに、モジュールレベル __all__ 変数(変数名のリスト)のPythonの決まり事を利用することによって、Fabricが "見る" ものを制限することができます。もし何かの理由によりデフォルトでは db.migrations.run タスクを表示させたくない場合、 db/migrations.py の一番上に以下を追加することができます:

__all__ = ['list']

ここには 'run' がないことに留意してください。もし必要なら run をこの階層のどこかに直接インポートすることも可能ですが、そうでなければ、隠れれたままになります。

ひとつ上へ

これまで、fabfileパッケージを片付いた状態に維持し、直接的な方法でインポートしてきましたが、ファイルシステムのレイアウトはここでは実際には考慮していません。Fabricのすべてのローダーが気にするのは、インポートされた時のモジュールに与えられた名称です。

例えば、ルートの __init__.py の市場うえを次のように変更すると:

import db as database

タスクリストは次のように変わります:

deploy
compress
lb.add_backend
database.migrations.list
database.migrations.run

これは他のどのインポートにも適用されます。サードパーティのモジュールを自分のタスク階層にインポートしたり、深くネストされたモジュールを取ってきてトップレベル近くに置くことも可能です。

ネストされたリスト出力

最後に、このセクションではデフォルトのFabric --list 出力を使用してきました。これにより実際のタスク名は何なのかがより明確になります。とは言え、--list-format オプションに nested を渡すと、よりネストされていたりツリーライクな表示を得られます:

$ fab --list-format=nested --list
Available commands (remember to call as module.[...].task):

    deploy
    compress
    lb:
        add_backend
    database:
        migrations:
            list
            run

"実際の" タスク名が少し分かりにくくなりますが、この表示は大規模な名前空間でのタスク構成を把握する簡単な方法を提供します。

クラシックなタスク

新しいスタイルの Task ベースのタスクが見つからない場合、Fabricはfabfile内の呼び出し可能などんなオブジェクトでも検討します。 ただし、 次を除きます:

  • アンダースコア(_)で始まる名称の呼び出し可能なオブジェクト。言い換えると、ここではPythonの通常の "プライベート" 規則が適用されます。
  • Fabric自身内で定義されている呼び出し可能なオブジェクト。 runsudo などのFabric自身の関数はタスクリストには表示されません。

インポート

Pythonの import ステートメントは事実上、あなたのモジュール名前空間にあるインポートされたオブジェクトを含みます。Fabricのfabfileは単なるPythonモジュールなので、インポートもまた、fabfile自身に定義されているすべてと一緒に出来る限りクラシックスタイルのタスクとみなされます。

注釈

これはインポートされた 呼び出し可能なオブジェクト のみに適用され、モジュールには適用されません。インポートされたモジュールは 新しいスタイルのタスク を含む場合のみ実行され、その時点でこのセクションは適用されません。

このため、私達としては後ろに module.callable() が続くインポートの import module 形式を利用するよう強くおすすめします。これにより、結果として from module import callable を行うよりもよりきれいなfabfile APIになります。

ウェブサービスから何らかのデータを取り出すために urllib.urlopen を使用しているサンプルのfabfileを例に上げましょう:

from urllib import urlopen

from fabric.api import run

def webservice_read():
    objects = urlopen('http://my/web/service/?foo=bar').read().split()
    print(objects)

これはかなり単純でエラー無しで実行できます。しかし、このfabfileで fab --list を実行するとどうなるか見てみましょう:

$ fab --list
Available commands:

  webservice_read   List some directories.
  urlopen           urlopen(url [, data]) -> open file-like object

1つのタスクしかないfabfileで2つの "タスク" が表示されています。これはよくありませんし、疑うことを知らないユーザーがうっかりと fab urlopen を呼びだそうとするかもしれませんし、たぶんまともには動作しないでしょう。実世界のfabfileを想像してみてください。かなり複雑になることが多いでしょうし、すぐに乱雑になってしまうことがわかってもらえると思います。

参考のため、以下がおすすめの方法になります:

import urllib

from fabric.api import run

def webservice_read():
    objects = urllib.urlopen('http://my/web/service/?foo=bar').read().split()
    print(objects)

簡単な変更ですが、このfabfileを使う方は誰でもこれにより少しは幸せになるでしょう。