urakataというscaffold command serverのようなものを作ろうとした話
urakataというものを作ろうとした話
OCamlをいじっていて何回同じようなMakefileを作っているんだろうというような思いが浮かんで。 scaffoldのようなコマンドを作ろうかと思ったのですが。scaffoldコマンドの管理がめんどくさい。 元となるtemplateのようなファイル構造が必要になるのだけれど。それをどこに置くかというので悩んでしまっていた。 何か良い方法はないかなと考えた結果。
もう。scaffoldもserviceになれば良いような気がするんだよなー。jsonもらって引数適用して実行するwokerがいて。json返すserverがいる。scaffoldのlistを調べるのは、自分が指定しているserverのリストにquery投げてマージ
— SyntaxError x = 'y', (@podhmo) 2015, 4月 4
serverの部分staticにできないかな。管理したくない
— SyntaxError x = 'y', (@podhmo) 2015, 4月 4
templateエンジンもworkerになっていれば良いんだよなー。jsonと引数もらって適用した文字列を返すような感じ。
— SyntaxError x = 'y', (@podhmo) 2015, 4月 4
あー。でも現在の環境の値欲しかったりする場合あるからtemplate engine実行するのがworkerだと辛い気もしてきた。
— SyntaxError x = 'y', (@podhmo) 2015, 4月 4
scaffoldがworkerになってくれると嬉しい
最終的にはscaffoldがworkerになってくれると嬉しい。例えば、jsonをpostするとそれに従ったscaffoldを返してくれるような何か。 と言っても、現実的かというとそうでもないような気がするので以下のような形にすることにした。
現状のステータス
web appを作ろうかとpyramidで作り始めたけれど。結局コマンドだけ実装するところで終わった。 一応、雛型のようなファイル構造からpythonスクリプトを生成するところまでは動く。
以下の様な形で使える。
$ urakata initialize development.ini # ファイルを調べてjsonを作成(これがclient側でもできないと本当はマズイ) $ urakata scan development.ini demo/season -overrides=demo/overrides.season.json > season.json # dbにjsonのデータを登録 $ urakata register development.ini season.json # scaffoldの生成 $ urakata codegen development.ini my-scaffold > scaffold.py $ python scaffold.py season autumn(default:):? aki month(default:):? gatsu INFO:__main__:emit[D] -- season INFO:__main__:emit[F] -- season/aki.txt spring(default:haru):? INFO:__main__:emit[F] -- season/haru.txt summer(default:natsu):? INFO:__main__:emit[F] -- season/natsu.txt winter(default:):? huyu INFO:__main__:emit[F] -- season/huyu.txt $ tree /tmp/season /tmp/season ├── aki.txt ├── haru.txt ├── huyu.txt └── natsu.txt 0 directories, 4 files $ cat /tmp/season/aki.txt aki - 9gatsu - 10gatsu - 11gatsu
不足しているもの
不足しているものは結構あって、web appとしての機能がまるまる無い。 後、本当は登録されているjsonデータから元となるファイル構造を取り出す処理というのも必要。 また、クライアントのスクリプト側でファイル構造を走査してjsonを作成する処理が実行できる必要がある。
appendix
それぞれ生成されるのは以下のようなファイル
// overrides.season.json (これは手書き) { "spring": "春", "summer": "夏" }
// season.json { "usages": {}, "name": "my-scaffold", "root": "demo/season", "defaults": { "spring": "春", "winter": "", "month": "", "summer": "夏", "autumn": "" }, "templates": [ { "encoding": "utf-8", "name": "+autumn+.txt.tmpl", "content": "{{autumn}}\n- 9{{month}}\n- 10{{month}}\n- 11{{month}}\n" }, { "encoding": "utf-8", "name": "+spring+.txt.tmpl", "content": "{{spring}}\n- 3{{month}}\n- 4{{month}}\n- 5{{month}}\n\n" }, { "encoding": "utf-8", "name": "+summer+.txt.tmpl", "content": "{{summer}}\n- 6{{month}}\n- 7{{month}}\n- 8{{month}}\n\n" }, { "encoding": "utf-8", "name": "+winter+.txt.tmpl", "content": "{{winter}}\n- 12{{month}}\n- 1{{month}}\n- 2{{month}}\n" } ], "version": "0.0.1", "parameters": [ "spring", "summer", "month", "autumn", "winter" ] }
import sys import os.path import re from collections import( defaultdict, Mapping, OrderedDict ) import logging logger = logging.getLogger(__name__) class reify(object): """ Use as a class method decorator. It operates almost exactly like the Python ``@property`` decorator, but it puts the result of the method it decorates into the instance dict after the first call, effectively replacing the function it decorates with an instance variable. It is, in Python parlance, a non-data descriptor. An example: .. code-block:: python class Foo(object): @reify def jammy(self): print('jammy called') return 1 And usage of Foo: >>> f = Foo() >>> v = f.jammy 'jammy called' >>> print(v) 1 >>> f.jammy 1 >>> # jammy func not called the second time; it replaced itself with 1 """ def __init__(self, wrapped): self.wrapped = wrapped try: self.__doc__ = wrapped.__doc__ except: # pragma: no cover pass def __get__(self, inst, objtype=None): if inst is None: return self val = self.wrapped(inst) setattr(inst, self.wrapped.__name__, val) return val class InputWrapper(object): def __init__(self, input_port): self.input_port = input_port def __getattr__(self, k): return getattr(self.input_port, k) def read(self): return input() class Emitter(object): def __init__(self, request, root, config, overrides=None): self.request = request self.root = root self.config = config self.overrides = overrides or {} @reify def env(self): return EmitEnv(self.overrides, self.config.defaults, self.config.usages) def emit(self): for name, content in self.config.contents.items(): self.emit_content(name, content) def emit_content(self, name, content): name_scanner = self.config.name_scanner template_scanner = self.config.template_scanner emit_name = name_scanner.replace(name, self.env) if template_scanner.is_template_name(name): content = template_scanner.replace(content, self.env) emit_name = template_scanner.normalize_name(emit_name) fullpath = os.path.join(self.root, emit_name) dirpath = os.path.dirname(fullpath) if not os.path.exists(dirpath): logger.info("emit[D] -- %s", dirpath) os.makedirs(dirpath) logger.info("emit[F] -- %s", fullpath) with open(fullpath, "w") as wf: wf.write(content) class EmitEnv(Mapping): def __init__(self, cache, defaults, usages, input_port=InputWrapper(sys.stdin), output_port=sys.stderr): self.cache = cache self.defaults = defaults self.usages = usages self.input_port = input_port self.output_port = output_port def __getitem__(self, k): try: return self.cache[k] except: self.cache[k] = self.read(k) return self.cache[k] def read(self, k): usage = self.usages.get(k) or "{name}(default:{default}):?\n".format(name=k, default=self.defaults.get(k, "")) while True: self.output_port.write(usage) self.output_port.flush() value = self.input_port.read().rstrip() or self.defaults.get(k) if value: return value def __iter__(self): return iter(self.cache) def __len__(self): return len(self.cache) class ScanConfig(object): def __init__(self, request, root, defaults=None, usages=None, contents=[]): self.request = request self.root = root self.parameters = set() self.defaults = defaults or defaultdict(str) self.usages = usages or defaultdict(str) self.contents = OrderedDict(contents) or OrderedDict() # name -> content def add_usage(self, name, usage): self.usages[name] = usage def add_default(self, name, default): self.defaults[name] = default def add_content(self, name, content): self.contents[name] = content def fill_defaults(self, v=""): for p in self.parameters: if p not in self.defaults: self.add_default(p, v) @reify def name_scanner(self): return NameScanner(self) @reify def template_scanner(self): return Jinja2Scanner(self) class NameScanner(object): def __init__(self, config): self.config = config rx = re.compile("\+([^\+]+)\+") def scan(self, filename): for k in self.rx.findall(filename): self.config.parameters.add(k) def replace(self, name, env): def repl(m): return env[m.group(1)] return self.rx.sub(repl, name) class Jinja2Scanner(object): def __init__(self, config): self.config = config @reify def environment(self): from jinja2.environment import Environment return Environment() # todo: input encoding, customize def is_template_name(self, name): return name.endswith(".tmpl") def normalize_name(self, name): return name.rsplit(".tmpl", 1)[0] def parse(self, content): from jinja2 import meta ast = self.environment.parse(content) return meta.find_undeclared_variables(ast) def scan(self, io): content = io.read() for k in self.parse(content): self.config.parameters.add(k) def replace(self, content, env): from jinja2 import Template from jinja2.utils import concat t = Template(content) return concat(t.root_render_func(t.new_context(env, shared=True))) def main(args): root = args[0] defaults = { "spring": "春", "autumn": "", "month": "", "summer": "夏", "winter": "" } usages = {} contents = [ [ "+autumn+.txt.tmpl", "{{autumn}}\n- 9{{month}}\n- 10{{month}}\n- 11{{month}}\n\n" ], [ "+spring+.txt.tmpl", "{{spring}}\n- 3{{month}}\n- 4{{month}}\n- 5{{month}}\n\n" ], [ "+summer+.txt.tmpl", "{{summer}}\n- 6{{month}}\n- 7{{month}}\n- 8{{month}}\n\n" ], [ "+winter+.txt.tmpl", "{{winter}}\n- 12{{month}}\n- 1{{month}}\n- 2{{month}}\n\n" ] ] config = ScanConfig(None, root, defaults=defaults, usages=usages, contents=contents) emitter = Emitter(None, root, config) emitter.emit() if __name__ == '__main__': logging.basicConfig(level=logging.INFO) main(sys.argv[1:])