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というファイルを作成します。典型的には以下の様になります。
- from __future__ import with_statement
- from tasktools import *
- global_description(u"""
- サンプルタスクファイルです。
- """)
- use_without_standard() # distutilsの標準コマンドを使用しないことを宣言します。
- load_path("./tasks") # "./tasks"以下のtasks.pyを再帰的に読み込みます
- with namespace("file") as ns:
- class mktmpfile(Task):
- u"""一時ファイルを作成します。
- """
- user_options = [("path=", "p", u"作成するパスです")]
- def run(self):
- print "create %s"%self.path
- def finalize_options(self):
- if not self.path: self.path = "/tmp/tmp.txt"
- class mklogfile(Task):
- u"""ログファイルを作成します。
- """
- def run(self):
- print "create log file"
- class init(Task):
- u"""ファイルを初期化します。
- """
- def run(self):
- pass
- sub_commands = [("file:mktmpfile", None),
- ("file:mklogfile", None)]
- if __name__ == "__main__":
- run()
ではpython tasks.py --help-commandsと実行してみましょう
############################################################
サンプルタスクファイルです。
############################################################
Commands:
file:init ファイルを初期化します。
sub commands:
file:mktmpfile
file:mklogfile
file:mklogfile ログファイルを作成します。
file:mktmpfile 一時ファイルを作成します。
usage: tasks.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: tasks.py --help [cmd1 cmd2 ...]
or: tasks.py --help-commands
or: tasks.py cmd --help
この様にglobal_descriptionで設定した説明と、定義したタスクの一覧が表示されます。
次に./tasks/tasks.pyを作成してみます。
- from __future__ import with_statement
- from tasktools import *
- use_without_standard()
- with namespace("subs") as ns:
- class test1(Task):
- u"""サブディレクトリで定義されたタスクです
- """
- def run(self):
- print "sub test"
- if __name__ == "__main__":
- run()
このファイルは./tasks.pyでload_path("./tasks")と宣言しているので./tasks.pyを実行すると自動的に読み込まれます。
もう一度python tasks.py --help-commandsと実行してみましょう
############################################################
サンプルタスクファイルです。
############################################################
Commands:
file:init ファイルを初期化します。
sub commands:
file:mktmpfile
file:mklogfile
file:mklogfile ログファイルを作成します。
file:mktmpfile 一時ファイルを作成します。
subs:test1 サブディレクトリで定義されたタスクです
usage: tasks.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: tasks.py --help [cmd1 cmd2 ...]
or: tasks.py --help-commands
or: tasks.py cmd --help
確かにsubs:test1コマンドが追加されています。
ではタスクを実行してみましょう。python tasks.py file:initを実行してみます。
running file:init
running file:mktmpfile
create /tmp/tmp.txt
running file:mklogfile
create log file
おお、実行されましたね。
user_optionsを定義しているタスクではオプションも渡せます。python tasks.py file:mktmpfile --path=/tmp/change.txtを実行してみましょう。
running file:mktmpfile
create /tmp/change.txt
ちゃんとオプションが渡されていますね。
こんな感じです。distutilsの独自コマンドに関する説明は46 新しいDistutilsコマンドの作成を参照してください。正直使えないページですが・・・。一応説明しておくとinitialize_optionsはuser_optionsの定義から自動生成するようになっています。また
distutils.core.Commandを継承しているのでこのクラスの機能も使えます。
せっかくCodereposに突っ込んだのでバグなんかが見つかったらガンガン直しちゃってください。
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でまとめてみてはいかがでしょうか。
PythonによるNESエミュレータ開発5
もうすぐ引越しです。頑張って部屋を片付けないと・・・
しばらくドタバタすると思し、これ以上作りこむモチベーションもないので、ここまで作ったものをあげておこうと思いました。
ダウンロード
pynes-0-0-1.zip
試し方
インストールはダウンロードしたzipファイルを展開するだけです。
必要なライブラリは
です。
両方ともeasy_install psyco、easy_install pygameでインストールできたはずです。
roms/以下に最低1つ以上ロムファイルを置いてください。現状、マッパーに対応してませんので、マッパー0のしか動く可能性はありません。現在動作を確認してるのは、前回あげさせていただいたTkShootくらいです。市販のはほとんど動かないんじゃないでしょうか。
一応参考までにあげておくと、動く可能性があるのはGolf,DonkeyKongなどです。
bin/pynesi.pyが起動用スクリプトです。コマンドラインから起動してください。起動したら、romファイルを番号で選択してください。
キーバーインドは
- 十字キー : カーソル
- スタート : テンキーの0
- セレクト : テンキーのEnter
- A : テンキーの3
- B : テンキーの2
になってます。キーバーインドを代えたい方はsrc/pynes/pad.pyを適当に書き換えてください。
- self.keymap1 = {
- K_UP : NES_PAD_UP,
- K_DOWN : NES_PAD_DOWN,
- K_LEFT : NES_PAD_LEFT,
- K_RIGHT : NES_PAD_RIGHT,
- K_KP0 : NES_PAD_START,
- K_KP_ENTER : NES_PAD_SELECT,
- K_KP2 : NES_PAD_B,
- K_KP3 : NES_PAD_A
- }
ここです。
とにかく、めちゃくちゃ遅いので、固まったと思ってもしばらくすると画面がちゃんと切り替わったりします。
よもや話
かなり適当です。前回(PythonによるNESエミュレータ開発4)から変わってません。マッパーっぽいのが用意してありますが、これはダミーです。他のエミュのソースを参考に必要そうな部分に適当にいれただけです。
一応、速度を重視しているものの、わかりやすく書いてるつもりなんで、Pythonが分かっていてかつ、エミュレータの基本的な構造が知りたい人には参考になるかもしれません。