Python,Rubyの言語内DSL構築力:PythonでRakeをまねる場合を例に
Python で Rake を真似るとしたらという反応を頂いたので、それにまつわるお話を。
まず、CodeReposにコミットしてあるtasktoolsはdistutils及びsetuptoolsを拡張するということを念頭に置いています。ですのである程度distutils.core.Commandの思想というか、インターフェイスを残しています。
- タスクをクラスで定義すること
- オプションの定義方法
sub_commandsやfinalize_optionsといったメソッド
などなどは元のまんまです。
じゃあ、distutilsやsetuptoolsを抜いて好きにRakeをPythonで真似るとしたら、というのが今回メインのお話。最初にこんな感じでコマンドを定義しますよ、というのを出してしまいます。
- from __future__ import with_statement
- from 今回作成したモジュール import *
- from subprocess import call
- # ここからタスク定義
- with namespace("test"):
- @task
- def build(t, depends = ["test:make"]):
- """builds this modules. """
- call(["ls"])
- print t.name, ": build done"
- @task
- def make(t):
- print "make"
- # 実行
- run()
Rakeだとこうなる。
- namespace "test" do
- desc "builds this modules"
- task "build" => "make" do |t|
- sh("ls")
- puts "#{t.name} done."
- end
- task "make" do |t|
- puts "make"
- end
- end
Python版もなかなかシンプルで見やすいと思うんですがどうでしょう。あとちょっとtips。さっきの記事だとos.systemを使ってますが、subprocessモジュールのcallやcheck_callのほうが便利じゃないかな、と思います。
タスクのメタ情報は引数のデフォルト値を使います。ここで@task(depends = ["test:make"])ってしたほうがいいんじゃないの?と思う人もいるかもしれません。真っ当な意見だと思いますし、それがPythonicなやり方と思います。しかしそうするとこうなります。
- with namespace("test"):
- @task(depends = ["test:make"])
- def build(t):
- """builds this modules. """
- call(["ls"])
- print t.name, ": build done"
- @task() #<= ここがキモくないですか?
- def make(t):
- print "make"
Pythonではproperty以外では()をつけないとメソッドが呼び出せません。だから引数が何もない場合は空のカッコがつくわけです(まぁDescriptorでhogehogeすればできるんですがね)。これ、イケてないですよね。というわけで引数のデフォルト値を使います。
ではこのタスク定義でタスクを実行するためのコードです。
- from __future__ import with_statement
- import inspect,new,sys
- from contextlib import contextmanager
- from optparse import OptionParser
- from itertools import *
- class _Tasks(dict):
- def __getitem__(self,key):
- if key in self: return dict.__getitem__(self, key)
- _abort("Error: Unknown task: %s"%key)
- class Task(object):
- def __init__(self, run, name, description,
- depends = None, options = None):
- self._run = new.instancemethod(run, self, self.__class__)
- self.name = _get_current_ns()+name
- self.description = description
- self.depends = depends or []
- self.options = options or []
- tasks[self.name] = self
- def run(self):
- for depend in self.depends:
- if tasks[depend].run() is False:
- _abort("Error: faild %s"%depend)
- return self._run()
- _namespace = []
- tasks = _Tasks()
- @contextmanager
- def namespace(name):
- _namespace.append(name)
- yield
- _namespace.pop()
- def task(f):
- names, _, _, values = inspect.getargspec(f)
- Task(f, f.__name__, f.__doc__ or "no description", **dict(izip(names[1:], values or [])))
- def print_tasks():
- maxlength = max(imap(len, tasks.iterkeys()))
- for name in sorted(tasks.iterkeys()):
- print name.ljust(maxlength), "#", tasks[name].description.strip()
- def run():
- usage = "usage :%prog [options] targets"
- parser = OptionParser(usage)
- parser.add_option("-T", "--tasks", action="store_true",
- dest="print_tasks",
- help="Display the tasks (matching optional PATTERN) with descriptions, then exit.")
- (options, args) = parser.parse_args()
- if options.print_tasks: _abort(print_tasks())
- if not args: _abort(parser.get_usage())
- map(lambda t:t.run(), imap(tasks.__getitem__, args))
- def _abort(msg):
- if msg : print msg
- sys.exit(1)
- def _get_current_ns(): return _namespace and ":".join(_namespace)+":" or ""
結構短いかな?。一応ちゃんと動きます。
tasks.py -T
test:build # builds this modules.
test:make # no description
tasks.py test:build
make
tasks.py
test:build : build done
うむ。
結局、Rubyの言語内DSL構築能力は
()が省略できる- ブロック(pythonではデコレータとwith_statementがありますが)
によるものが多いと思っています。ことPythonとの対比では。「シンプルさ」「書きやすさ」なんかは好みかな、と(Rakeの例だとdocstringを持っている分Pythonの方がいい感じさえしてきます)。例えばendってなんだよ、って言う人にはPythonのほうがウケがいいかもしれませんしそうじゃないかもしれません。
そんなこんなで試しに書いてみたらいい感じだったんで、ちゃんと本格的にPython版Rakeとして開発を続けてみようかなあ。
ところで言語内DSLといえばScalaでしょう。Rubyなんてちょろいもんですよ。なんてったってScalaは(ry
About this entry
You’re currently reading “Python,Rubyの言語内DSL構築力:PythonでRakeをまねる場合を例に,” an entry on inforno
- Published:
- 02.21.08 / 6pm




No comments
Jump to comment form | trackback uri [?]