Python標準モジュールでRakeもどき
誰もが一度使うと便利さと気軽さに感動する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 = None
- def finalize_options(self):
- if not self.host: raise "Error"
- setup(cmdclass={"my_command":MyCommand})
といった具合です。非常にPythonicなやり方ですね。ただ、いろいろオーバーライドしないといけないので面倒です。そこで、俺がコピペで使っている自家製テンプレの登場です。
もっと簡単にdistutilsでタスク
30行程度のテンプレを足して、簡単にタスクを定義できるようにしましょう。namespace機能はPython2.5以上限定です。
- from __future__ import with_statement
- from distutils.core import Command, setup
- from 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] = cls
- return cls
- class BaseCommand(Command): __metaclass__ = CommandType
- @contextmanager
- def namespace(name):
- _namespace.append(name)
- yield _get_ns()
- _namespace.pop()
これだけです。これでかなり簡単にタスクが作れるようになります。例を示しましょう。
- # task.py
- with 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.host
- print self.force
- print "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-commands
Standard commands:
build build everything needed to install
build_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 extensions
build_scripts "build" scripts (copy and fixup #! line)
clean clean up temporary files from 'build' command
install install everything from build directory
install_lib install all Python modules (extensions and pure Python)
install_headers install C/C++ header files
install_scripts install scripts (Python or otherwise)
install_data install data files
sdist create a source distribution (tarball, zip file, etc.)
register register the distribution with the Python package index
bdist create a built (binary) distribution
bdist_dumb create a "dumb" built distribution
bdist_rpm create an RPM distribution
bdist_wininst create an executable installer for MS Windows
Extra commands:
db_test3 test3 discription
db_create_test2 test2 discription
db_create_test1 test1 discription
test4 test4 discription
usage: task.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: task.py --help [cmd1 cmd2 ...]
or: task.py --help-commands
or: task.py cmd --help
こんな感じで、標準コマンドに加え「Extra commands」という形で定義したタスクが使えるようになります。もちろん、distutilsのビルド機能、パッケージ管理機能も使えますよ。
db_create_test1のヘルプを見てみましょう。
# python task.py db_create_test1 --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
Options for 'db_create_test1' command:
--host (-h) hostname
--port (-p) port
--force (-f) force execute
usage: task.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: task.py --help [cmd1 cmd2 ...]
or: task.py --help-commands
or: task.py cmd --help
ちゃんとオプションが表示されていますね。最後に、db_create_test1タスクを実行してみましょう。
# python task.py db_create_test1 --host=localhost
running db_create_test1
running db_create_test2
hello, Test2
localhost
None
hello, Test1
サブコマンドのdb_create_test2が実行されていること、きちんとオプションがselfの属性として設定されていることがわかりますね。
標準的な機能だけでもRakeのようなタスクはつくれますが、30行程度の工夫で断然便利になります。Rakeと違いrakeコマンドのような外部コマンドも必要なく、pythonのスクリプト1つで実現できるのも手軽です。というわけで、雑多なタスクはdistutilsでまとめてみてはいかがでしょうか。
About this entry
You’re currently reading “Python標準モジュールでRakeもどき,” an entry on inforno
- Published:
- 01.23.08 / 4am
- Tags:
- development python




3 Comments
Jump to comment form | trackback uri [?]