Python版Rake「tasktools」をCodeReposにコミットした

以前紹介したPython版Rakeもどきを改良してCodeReposに突っ込みました。まだ100行くらいです。自分では一番使っている自作モジュールなのでそろそろまとめようと思っていたのです。

http://coderepos.org/share/browser/lang/python/tasktools/trunk/tasktools.py

改良点

  • setuptoolsがあるときはそっちを使うようにした。
  • use_without_standardメソッドによってdistutils標準タスクを消すことができる。これによって--help-commandsの画面がすっきりする。また、この場合名前空間のセパレータに:を用いるようになった。
  • global_descriptionによってタスクファイル自体に説明がつけられるようになった。
  • load_pathメソッドで指定したディレクトリ以下を再帰的に検索し「tasks.py」という名前のファイルを読み込むことができるようになった。
  • --help-commandsで表示されるコマンドの並び順をソートするようにした。
  • --help-commandsでサブコマンドを一覧表示するようにした。

使用方法

以前書いたのとほとんど同じなんですが、まとめなおしておきます。

tasktoolsとは?

distutilsおよびsetuptoolsを拡張してextra commandを簡単に作成するためのユーティリティです。RubyにおけるRakeのようなものです。ビルド機能がほしい場合はdistutils, setuptoolsの標準ビルド機能、もしくはSConsと組み合わせるとハッピーになれます。

ちなみに、tasktoolsというのは同じくdistutilsの拡張であるsetuptoolsの命名規則に習っています。task機能を強化するからtasktoolsです。

チュートリアル

典型的なtasktoolsの使い方です。

まずtasks.pyというファイルを作成します。典型的には以下の様になります。

python code
  1. from __future__ import with_statement
  2. from tasktools import *
  3.  
  4. global_description(u"""
  5. サンプルタスクファイルです。
  6. """)
  7.  
  8. use_without_standard() # distutilsの標準コマンドを使用しないことを宣言します。
  9. load_path("./tasks") # "./tasks"以下のtasks.pyを再帰的に読み込みます
  10.  
  11.  
  12. with namespace("file") as ns:
  13.   class mktmpfile(Task):
  14.     u"""一時ファイルを作成します。
  15. """
  16.     user_options = [("path=", "p", u"作成するパスです")]
  17.     def run(self):
  18.       print "create %s"%self.path
  19.  
  20.     def finalize_options(self):
  21.       if not self.path: self.path = "/tmp/tmp.txt"
  22.  
  23.   class mklogfile(Task):
  24.     u"""ログファイルを作成します。
  25. """
  26.     def run(self):
  27.       print "create log file"
  28.  
  29.   class init(Task):
  30.     u"""ファイルを初期化します。
  31. """
  32.     def run(self):
  33.       pass
  34.  
  35.     sub_commands = [("file:mktmpfile", None),
  36.                     ("file:mklogfile", None)]
  37.  
  38. if __name__ == "__main__":
  39.   run()
  40.  

ではpython tasks.py --help-commandsと実行してみましょう

 code
  1. ############################################################
  2.  
  3. サンプルタスクファイルです。
  4.  
  5. ############################################################
  6.  
  7. Commands:
  8.   file:init ファイルを初期化します。
  9.         sub commands:
  10.                 file:mktmpfile
  11.                 file:mklogfile
  12.   file:mklogfile ログファイルを作成します。
  13.   file:mktmpfile 一時ファイルを作成します。
  14.  
  15. usage: tasks.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  16.   or: tasks.py --help [cmd1 cmd2 ...]
  17.   or: tasks.py --help-commands
  18.   or: tasks.py cmd --help
  19.  

この様にglobal_descriptionで設定した説明と、定義したタスクの一覧が表示されます。

次に./tasks/tasks.pyを作成してみます。

python code
  1. from __future__ import with_statement
  2. from tasktools import *
  3.  
  4. use_without_standard()
  5. with namespace("subs") as ns:
  6.   class test1(Task):
  7.     u"""サブディレクトリで定義されたタスクです
  8. """
  9.     def run(self):
  10.       print "sub test"
  11.  
  12. if __name__ == "__main__":
  13.   run()
  14.  

このファイルは./tasks.pyload_path("./tasks")と宣言しているので./tasks.pyを実行すると自動的に読み込まれます。 もう一度python tasks.py --help-commandsと実行してみましょう

 code
  1. ############################################################
  2.  
  3. サンプルタスクファイルです。
  4.  
  5. ############################################################
  6.  
  7. Commands:
  8.   file:init ファイルを初期化します。
  9.         sub commands:
  10.                 file:mktmpfile
  11.                 file:mklogfile
  12.   file:mklogfile ログファイルを作成します。
  13.   file:mktmpfile 一時ファイルを作成します。
  14.   subs:test1 サブディレクトリで定義されたタスクです
  15.  
  16. usage: tasks.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  17.   or: tasks.py --help [cmd1 cmd2 ...]
  18.   or: tasks.py --help-commands
  19.   or: tasks.py cmd --help
  20.  

確かにsubs:test1コマンドが追加されています。

ではタスクを実行してみましょう。python tasks.py file:initを実行してみます。

 code
  1. running file:init
  2. running file:mktmpfile
  3. create /tmp/tmp.txt
  4. running file:mklogfile
  5. create log file
  6.  

おお、実行されましたね。

user_optionsを定義しているタスクではオプションも渡せます。python tasks.py file:mktmpfile --path=/tmp/change.txtを実行してみましょう。

 code
  1. running file:mktmpfile
  2. create /tmp/change.txt
  3.  

ちゃんとオプションが渡されていますね。


こんな感じです。distutilsの独自コマンドに関する説明は46 新しいDistutilsコマンドの作成を参照してください。正直使えないページですが・・・。一応説明しておくとinitialize_optionsはuser_optionsの定義から自動生成するようになっています。またdistutils.core.Commandを継承しているのでこのクラスの機能も使えます。

せっかくCodereposに突っ込んだのでバグなんかが見つかったらガンガン直しちゃってください。

07.27.08/12am

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でまとめてみてはいかがでしょうか。

07.27.08/12am

PythonによるNESエミュレータ開発5

もうすぐ引越しです。頑張って部屋を片付けないと・・・

しばらくドタバタすると思し、これ以上作りこむモチベーションもないので、ここまで作ったものをあげておこうと思いました。

ダウンロード
pynes-0-0-1.zip

試し方

インストールはダウンロードしたzipファイルを展開するだけです。

必要なライブラリは

です。
両方ともeasy_install psycoeasy_install pygameでインストールできたはずです。

roms/以下に最低1つ以上ロムファイルを置いてください。現状、マッパーに対応してませんので、マッパー0のしか動く可能性はありません。現在動作を確認してるのは、前回あげさせていただいたTkShootくらいです。市販のはほとんど動かないんじゃないでしょうか。

一応参考までにあげておくと、動く可能性があるのはGolf,DonkeyKongなどです。

bin/pynesi.pyが起動用スクリプトです。コマンドラインから起動してください。起動したら、romファイルを番号で選択してください。

キーバーインドは

  • 十字キー : カーソル
  • スタート : テンキーの0
  • セレクト : テンキーのEnter
  • A : テンキーの3
  • B : テンキーの2

になってます。キーバーインドを代えたい方はsrc/pynes/pad.pyを適当に書き換えてください。

python code
  1.     self.keymap1 = {
  2.       K_UP : NES_PAD_UP,
  3.       K_DOWN : NES_PAD_DOWN,
  4.       K_LEFT : NES_PAD_LEFT,
  5.       K_RIGHT : NES_PAD_RIGHT,
  6.       K_KP0 : NES_PAD_START,
  7.       K_KP_ENTER : NES_PAD_SELECT,
  8.       K_KP2 : NES_PAD_B,
  9.       K_KP3 : NES_PAD_A
  10.     }
  11.  

ここです。

とにかく、めちゃくちゃ遅いので、固まったと思ってもしばらくすると画面がちゃんと切り替わったりします。

よもや話

かなり適当です。前回(PythonによるNESエミュレータ開発4)から変わってません。マッパーっぽいのが用意してありますが、これはダミーです。他のエミュのソースを参考に必要そうな部分に適当にいれただけです。

一応、速度を重視しているものの、わかりやすく書いてるつもりなんで、Pythonが分かっていてかつ、エミュレータの基本的な構造が知りたい人には参考になるかもしれません。

07.27.08/12am

About

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

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

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

Pages