誰もが一度使うと便利さと気軽さに感動するRubyが誇るライブラリ、Rake。
プログラムのビルドもそうなんですが、雑多なタスクを簡単に書けて、整理できるのがなんといっても魅力的。RailsなんかではDBの作成から何から、ばんばんRakeタスクにされていますよね。
さて、俺はPythonistasなので、PythonでRakeみたいなのがほしいわけです。ビルドに限っていえばPythonはSconsという素晴らしいツールがあります。C言語はおろか、JAVA、PDF、PostScriptなどなど、さらにはSubversionもサポートしていますし、並列コンパイルもでき、実績も多数で申し分ありません。
でも俺がしたいのは、雑多なタスクを放り込む、コレ。そういうのを簡単にやるライブラリってPythonではないんでしょうか。RubyではRakeが標準添付されるというのに。いえ、あります。前からあるんです。それがdistutils。
distutils(およびその拡張のsetuptools)はPythonの標準的なパッケージ配布システムとして有名です。でも実は自分でコマンドを定義してRakeのように使うことができることはあまりしられていないのではないでしょうか。
distutilsでコマンドを定義する
distutilsでコマンドを定義するにはdistutils.core.Commandクラスを継承し、色々オーバーライドします。説明もつけられますし、オプションもとれて、sub_commandsで依存関係(コマンド分割)を設定することもできます。
class MyCommand(Command):description = "コマンドの説明"user_options = [("host=", "h", "hostname")] # オプションsub_commands = [("pre_task", None)] # サブコマンドdef run(self): pass # 実行する内容def initialize_options(self): # オプションの初期化self.host = Nonedef finalize_options(self):if not self.host: raise "Error"setup(cmdclass={"my_command":MyCommand})
といった具合です。非常にPythonicなやり方ですね。ただ、いろいろオーバーライドしないといけないので面倒です。そこで、俺がコピペで使っている自家製テンプレの登場です。
もっと簡単にdistutilsでタスク
30行程度のテンプレを足して、簡単にタスクを定義できるようにしましょう。namespace機能はPython2.5以上限定です。
from __future__ import with_statementfrom distutils.core import Command, setupfrom contextlib import contextmanager_cmds = {}_namespace = []_get_ns = lambda:_namespace and "_".join(_namespace)+"_" or ""class CommandType(type):def __new__(cls, class_name, class_bases, classdict):d = dict(user_options=[], finalize_options=lambda s:None)d.update(classdict)def _(self):[setattr(self,i[0].rstrip("="),None) for i in d["user_options"]]d["initialize_options"] = _d["boolean_options"] = [i for i,j,k in d["user_options"] if not i.endswith("=")]def _(self):map(self.run_command, self.get_sub_commands())return classdict["run"](self)d["run"] = _name = _get_ns()+class_name.lower()cls = type.__new__(cls, name, class_bases + (object,), d)cls.description = cls.__doc__if class_name != "BaseCommand" : _cmds[name] = clsreturn clsclass BaseCommand(Command): __metaclass__ = CommandType@contextmanagerdef namespace(name):_namespace.append(name)yield _get_ns()_namespace.pop()
これだけです。これでかなり簡単にタスクが作れるようになります。例を示しましょう。
# task.pywith namespace("db") as ns:with namespace("create") as ns:class Test1(BaseCommand):"""test1 discription"""user_options = [ ("host=", "h", "hostname"),("port=", "p", "port"),("force", "f", "force execute")]def run(self):print self.hostprint self.forceprint "hello, ", self.__class__.__name__sub_commands = [(ns+"test2",None)]class Test2(BaseCommand):"""test2 discription"""def run(self):print "hello, ", self.__class__.__name__class Test3(BaseCommand):"""test3 discription"""def run(self):print "hello, ", self.__class__.__name__class Test4(BaseCommand):"""test4 discription"""user_options = [ ("host=", "h", "hostname") ]def run(self):print "hello, ", self.__class__.__name__def finalize_options(self):if not self.host:raise ValueError("host must not be None")setup(cmdclass=_cmds)
かなり直感的になったのではないでしょうか。withを使えば名前空間も結構簡単に実現できます。コマンドのヘルプを見てみましょう。
# python task.py --help-commandsStandard commands:build build everything needed to installbuild_py "build" pure Python modules (copy to build directory)build_ext build C/C++ extensions (compile/link to build directory)build_clib build C/C++ libraries used by Python extensionsbuild_scripts "build" scripts (copy and fixup #! line)clean clean up temporary files from 'build' commandinstall install everything from build directoryinstall_lib install all Python modules (extensions and pure Python)install_headers install C/C++ header filesinstall_scripts install scripts (Python or otherwise)install_data install data filessdist create a source distribution (tarball, zip file, etc.)register register the distribution with the Python package indexbdist create a built (binary) distributionbdist_dumb create a "dumb" built distributionbdist_rpm create an RPM distributionbdist_wininst create an executable installer for MS WindowsExtra commands:db_test3 test3 discriptiondb_create_test2 test2 discriptiondb_create_test1 test1 discriptiontest4 test4 discriptionusage: task.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]or: task.py --help [cmd1 cmd2 ...]or: task.py --help-commandsor: task.py cmd --help
こんな感じで、標準コマンドに加え「Extra commands」という形で定義したタスクが使えるようになります。もちろん、distutilsのビルド機能、パッケージ管理機能も使えますよ。
db_create_test1のヘルプを見てみましょう。
# python task.py db_create_test1 --helpCommon commands: (see '--help-commands' for more)setup.py build will build the package underneath 'build/'setup.py install will install the packageGlobal 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 messageOptions for 'db_create_test1' command:--host (-h) hostname--port (-p) port--force (-f) force executeusage: task.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]or: task.py --help [cmd1 cmd2 ...]or: task.py --help-commandsor: task.py cmd --help
ちゃんとオプションが表示されていますね。最後に、db_create_test1タスクを実行してみましょう。
# python task.py db_create_test1 --host=localhostrunning db_create_test1running db_create_test2hello, Test2localhostNonehello, Test1
サブコマンドのdb_create_test2が実行されていること、きちんとオプションがselfの属性として設定されていることがわかりますね。
標準的な機能だけでもRakeのようなタスクはつくれますが、30行程度の工夫で断然便利になります。Rakeと違いrakeコマンドのような外部コマンドも必要なく、pythonのスクリプト1つで実現できるのも手軽です。というわけで、雑多なタスクはdistutilsでまとめてみてはいかがでしょうか。
3 comments
trackback uriPerlクックブックのお題をPythonで解いてみた その31 [みんなのPython Webアプリ編] P.49 Webアプリケーションに値を渡す Python標準 (more...)
http://pythonpaste.org/script/developer.html
Pasteは知っていました。フレームワークで使われていたりしますよね。
PasteはWEB指向ですよね。また、書き方がdistutilsとかなり似ていて、記述量も大して変わらないので、ローカルでいろいろやるためにPasteを使うのはちょっと・・・と思っています。
Leave a Comment