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で依存関係(コマンド分割)を設定することもできます。

python code
  1. class MyCommand(Command):
  2.   description = "コマンドの説明"
  3.   user_options = [("host=", "h", "hostname")] # オプション
  4.   sub_commands = [("pre_task", None)] # サブコマンド
  5.   def run(self): pass # 実行する内容
  6.   def initialize_options(self): # オプションの初期化
  7.     self.host = None
  8.   def finalize_options(self):
  9.     if not self.host: raise "Error"
  10.  
  11. setup(cmdclass={"my_command":MyCommand})
  12.  

といった具合です。非常にPythonicなやり方ですね。ただ、いろいろオーバーライドしないといけないので面倒です。そこで、俺がコピペで使っている自家製テンプレの登場です。

もっと簡単にdistutilsでタスク

30行程度のテンプレを足して、簡単にタスクを定義できるようにしましょう。namespace機能はPython2.5以上限定です。

python code
  1. from __future__ import with_statement
  2. from distutils.core import Command, setup
  3. from contextlib import contextmanager
  4.  
  5. _cmds = {}
  6. _namespace = []
  7. _get_ns = lambda:_namespace and "_".join(_namespace)+"_" or ""
  8. class CommandType(type):
  9.   def __new__(cls, class_name, class_bases, classdict):
  10.     d = dict(user_options=[], finalize_options=lambda s:None)
  11.     d.update(classdict)
  12.     def _(self):
  13.       [setattr(self,i[0].rstrip("="),None) for i in d["user_options"]]
  14.     d["initialize_options"] = _
  15.     d["boolean_options"] = [i for i,j,k in d["user_options"] if not i.endswith("=")]
  16.     def _(self):
  17.       map(self.run_command, self.get_sub_commands())
  18.       return classdict["run"](self)
  19.     d["run"] = _
  20.     name = _get_ns()+class_name.lower()
  21.     cls = type.__new__(cls, name, class_bases + (object,), d)
  22.     cls.description = cls.__doc__
  23.     if class_name != "BaseCommand" : _cmds[name] = cls
  24.     return cls
  25. class BaseCommand(Command): __metaclass__ = CommandType
  26.  
  27. @contextmanager
  28. def namespace(name):
  29.   _namespace.append(name)
  30.   yield _get_ns()
  31.   _namespace.pop()
  32.  

これだけです。これでかなり簡単にタスクが作れるようになります。例を示しましょう。

python code
  1. # task.py
  2. with namespace("db") as ns:
  3.   with namespace("create") as ns:
  4.     class Test1(BaseCommand):
  5.       """test1 discription
  6. """
  7.       user_options = [ ("host=", "h", "hostname"),
  8.                       ("port=", "p", "port"),
  9.                       ("force", "f", "force execute")]
  10.       def run(self):
  11.         print self.host
  12.         print self.force
  13.         print "hello, ", self.__class__.__name__
  14.  
  15.       sub_commands = [(ns+"test2",None)]
  16.  
  17.     class Test2(BaseCommand):
  18.       """test2 discription
  19. """
  20.       def run(self):
  21.         print "hello, ", self.__class__.__name__
  22.  
  23.  
  24.   class Test3(BaseCommand):
  25.     """test3 discription
  26. """
  27.     def run(self):
  28.       print "hello, ", self.__class__.__name__
  29.  
  30. class Test4(BaseCommand):
  31.   """test4 discription
  32. """
  33.   user_options = [ ("host=", "h", "hostname") ]
  34.   def run(self):
  35.     print "hello, ", self.__class__.__name__
  36.  
  37.   def finalize_options(self):
  38.     if not self.host:
  39.       raise ValueError("host must not be None")
  40.  
  41.  
  42. setup(cmdclass=_cmds)
  43.  

かなり直感的になったのではないでしょうか。withを使えば名前空間も結構簡単に実現できます。コマンドのヘルプを見てみましょう。

 code
  1. # python task.py --help-commands
  2. Standard commands:
  3.   build build everything needed to install
  4.   build_py "build" pure Python modules (copy to build directory)
  5.   build_ext build C/C++ extensions (compile/link to build directory)
  6.   build_clib build C/C++ libraries used by Python extensions
  7.   build_scripts "build" scripts (copy and fixup #! line)
  8.   clean clean up temporary files from 'build' command
  9.   install install everything from build directory
  10.   install_lib install all Python modules (extensions and pure Python)
  11.   install_headers install C/C++ header files
  12.   install_scripts install scripts (Python or otherwise)
  13.   install_data install data files
  14.   sdist create a source distribution (tarball, zip file, etc.)
  15.   register register the distribution with the Python package index
  16.   bdist create a built (binary) distribution
  17.   bdist_dumb create a "dumb" built distribution
  18.   bdist_rpm create an RPM distribution
  19.   bdist_wininst create an executable installer for MS Windows
  20.  
  21. Extra commands:
  22.   db_test3 test3 discription
  23.  
  24.   db_create_test2 test2 discription
  25.  
  26.   db_create_test1 test1 discription
  27.  
  28.   test4 test4 discription
  29.  
  30. usage: task.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  31.   or: task.py --help [cmd1 cmd2 ...]
  32.   or: task.py --help-commands
  33.   or: task.py cmd --help
  34.  

こんな感じで、標準コマンドに加え「Extra commands」という形で定義したタスクが使えるようになります。もちろん、distutilsのビルド機能、パッケージ管理機能も使えますよ。

db_create_test1のヘルプを見てみましょう。

 code
  1. # python task.py db_create_test1 --help
  2. Common commands: (see '--help-commands' for more)
  3.  
  4.   setup.py build will build the package underneath 'build/'
  5.   setup.py install will install the package
  6.  
  7. Global options:
  8.   --verbose (-v) run verbosely (default)
  9.   --quiet (-q) run quietly (turns verbosity off)
  10.   --dry-run (-n) don't actually do anything
  11.   --help (-h) show detailed help message
  12.  
  13. Options for 'db_create_test1' command:
  14.   --host (-h) hostname
  15.   --port (-p) port
  16.   --force (-f) force execute
  17.  
  18. usage: task.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  19.   or: task.py --help [cmd1 cmd2 ...]
  20.   or: task.py --help-commands
  21.   or: task.py cmd --help
  22.  

ちゃんとオプションが表示されていますね。最後に、db_create_test1タスクを実行してみましょう。

 code
  1. # python task.py db_create_test1 --host=localhost
  2. running db_create_test1
  3. running db_create_test2
  4. hello, Test2
  5. localhost
  6. None
  7. hello, Test1
  8.  

サブコマンドのdb_create_test2が実行されていること、きちんとオプションがselfの属性として設定されていることがわかりますね。


標準的な機能だけでもRakeのようなタスクはつくれますが、30行程度の工夫で断然便利になります。Rakeと違いrakeコマンドのような外部コマンドも必要なく、pythonのスクリプト1つで実現できるのも手軽です。というわけで、雑多なタスクはdistutilsでまとめてみてはいかがでしょうか。

Related posts:

07.27.08/12am

3 comments

trackback uri
  • ajax-loading
  • ajax-loading
  • ajax-loading
  1. 常山日記 01.23.08/03pm website
    [Python]巡回...

    Perlクックブックのお題をPythonで解いてみた その31 [みんなのPython Webアプリ編] P.49 Webアプリケーションに値を渡す Python標準 (more...)
  2. aodag 02.29.08/12am website
    すでにご存知かもしれませんが、PasteScriptという雑多なタスクを自動化するモジュールがあります。
    http://pythonpaste.org/script/developer.html
  3. yuin 03.04.08/02pm
    コメント有難う御座います。
    Pasteは知っていました。フレームワークで使われていたりしますよね。

    PasteはWEB指向ですよね。また、書き方がdistutilsとかなり似ていて、記述量も大して変わらないので、ローカルでいろいろやるためにPasteを使うのはちょっと・・・と思っています。

Leave a Comment

You can use these tags: <code>, <i>, <em>, <strong>, <a>

About

Author:yuin(http://inforno.net/)

文学部文化学科卒という生粋の文系趣味プログラマ。

主にRuby、Javascript、PHP、JAVA,Python,C,Scala,Schemeなどを使っています。今はPythonな感じかもしれない。今後作曲活動なども復活するかもしれない。

Pages