jinja2の設計
これはjinja2の設計の紹介であり、壮大な"hello, world"の実装と見ることもできます。非常に簡略化したjinja2(=MY_jinja2)を使って、おおよその構造をソースコードで示したい(あまり日本語では説明できない)のですが、そのためにはせめてjinja2と同じように動作することを確かめた方が良いでしょう。
#!/usr/bin/env python2.6 import unittest from test import test_support import MY_jinja2 import jinja2 class APITests(unittest.TestCase): def test_from_data(self): env = self.MODULE.Environment() tmpl = env.from_string('hello, world') self.assertEqual('hello, world', tmpl.render()) class API_jinja2Tests(APITests): MODULE = jinja2 class API_MY_jinja2Tests(APITests): MODULE = MY_jinja2 if __name__ == '__main__': test_support.run_unittest(API_jinja2Tests, API_MY_jinja2Tests)
これは、"hello, world"を返すだけのものなので、あまり説得力の無いテストですが、ここでは気にしないことにします。import MY_jinja2はMY_jinja2ディレクトリにある__init__.pyを呼んでます。この中身はこのようになっています。
- MY_jinja2/__init__.py
from MY_jinja2.environment import Template, Environment from MY_jinja2.compiler import CodeGenerator from MY_jinja2 import nodes, lexer, parser
そして、テストコードはEnvironmentオブジェクトを生成して、from_stringを読んでいます。このクラスは__init__.pyを見る限り、environment.pyに書かれています。
- MY_jinja2/environment.py
class Template(object): @classmethod def from_code(cls, code): tpl = object.__new__(cls) namespace = {} exec code in namespace tpl.render_func = namespace['root'] return tpl def render(self, *args, **kwargs): dic = dict(*args, **kwargs) return ''.join(self.render_func(dic)) from MY_jinja2 import lexer, parser, compiler class Environment: @classmethod def from_string(cls, source): tokens = lexer.Lexer.tokenize(source) stream = lexer.TokenStream(tokens) node = parser.Parser.parse(stream) generator = compiler.CodeGenerator() generator.visit(node) return Template.from_code(generator.get_code())
メソッドfrom_stringを見ると、ここに全ての処理の流れが書かれています(jinja2はパーサークラスを置き換えなど、カスタマイズする目的のため、ずいぶん違った印象を受けますが、本質的には同じです。)。lexer.Lexerが文字列(basestring)をトークンに変換し、parser.ParserがAstNodeに変換します。AstNodeVisitorであるcompiler.CodeGeneratorがnodeを辿りながらcode(中身はpython code)を生成。このcodeからTemplateを生成します。この順番に、ソースコードを見ていきます。
- MY_jinja2/lexer.py
import operator TOKEN_NAME = 'name' TOKEN_DATA = 'data' TOKEN_INITIAL = 'initial' TOKEN_EOF = 'eof' class Token(tuple): type, value = (property(operator.itemgetter(x)) for x in range(2)) def __new__(cls, type, value): return tuple.__new__(cls, (type, value)) class TokenStream: def __init__(self, iter_tokens): self._next = iter(iter_tokens).next self.current = Token(TOKEN_INITIAL, '') next(self) def __nonzero__(self): return self.current.type is not TOKEN_EOF def next(self): token = self.current if token.type is not TOKEN_EOF: try: self.current = self._next() except StopIteration: self.current = Token(TOKEN_EOF, '') return token import re class Lexer: @classmethod def tokenize(cls, source): regex = re.compile('(.+)', re.M| re.S) while 1: m = regex.match(source) yield Token(TOKEN_DATA, m.group()) if len(source) <= m.end(): break
Lexer.tokenizeの実装はひどいものがありますが、まだTOKEN_DATAしか扱えないだけで、正規表現を使ってルールを追加していけば、本物に近づきます。
- MY_jinja2/nodes.py
class Template: def __init__(self, body): self.body = body class TemplateData: def __init__(self, data): self.data = data class Output: def __init__(self, node): self.node = node
jinja2では、nodeは全てNodeを継承しています。複雑な式を扱うようになって、Nodeの種類が増えて、共通のメソッドなどが必要になったら、そのように書き換えるべきです。
- MY_jinja2/parser.py
from MY_jinja2 import nodes from MY_jinja2.lexer import TOKEN_DATA class Parser: @classmethod def parse(cls, stream): body = [] while stream: token = next(stream) assert(TOKEN_DATA == token.type) data = nodes.TemplateData(token.value) body.append(nodes.Output(data)) return nodes.Template(body)
Parser.parseも、すでにデータが来ることを知っています。これも拡張が必要です。
- My_jinja2/compiler.py
import StringIO class CodeGenerator: def __init__(self): self.is_first = True self.indent_level = 0 self.stream = StringIO.StringIO() def get_code(self): return self.stream.getvalue() def indent(self): self.indent_level += 1 def outdent(self): self.indent_level -= 1 def visit_TemplateData(self, node): self.stream.write(repr(unicode(node.data))) def visit_Output(self, node): if not self.is_first: self.stream.write('\n') self.stream.write(' ' *self.indent_level) self.stream.write('yield ') self.visit_TemplateData(node.node) self.is_first = False def visit_Template(self, node): self.stream.write('def root(dic):\n') self.indent() for child in node.body: self.visit_Output(child) self.outdent() visit = visit_Template
nodeに合わせてstreamフィールドにcodeを書き込んでいくように書きます。jinja2はvisitはvisit_(クラス名)を実行する(visitorパターン)ように書いていますが、まだそのようになっていません。