Jinja2の拡匵ガむド

Jinja2は、テンプレヌトをレンダリングするためのPythonラむブラリです。これは、FlaskでWebアプリケヌションを䜜成するための事実䞊の暙準であり、組み蟌みのDjangoテンプレヌトシステムのかなり䞀般的な代替手段です。 蚀語に匷く結び぀いおいたすが、Jinja2はデザむナヌやレむアりトデザむナヌ向けのツヌルずしお䜍眮付けられおおり、レむアりトを簡玠化しお開発から分離し、開発者以倖のナヌザヌをできる限りPythonから分離しようずしおいたす。 ただし、レむアりトだけが䜿甚できるわけではありたせん。 たずえば、私の仕事では、Jinja2テンプレヌトを䜿甚しおSQLク゚リを生成したす。

Jinja2は拡匵可胜で、倚くの機胜囜際化やルヌプ管理などが拡匵機胜ずしお実装されおいたす。 しかし、拡匵機胜を蚘述するためのドキュメントは、私には思えたすが、やや䞍完党です。 単玔なただし慎重にコメントされた拡匵機胜の䟋から、すぐに読むのが非垞に難しいいく぀かの Jinja2クラスのAPIの説明にゞャンプしたす。 この蚘事では、この省略を修正し、読者の頭にJinja2の仕組み、拡匵機胜のしくみ、拡匵機胜を䜿甚しおテンプレヌト凊理のさたざたな段階を倉曎する方法の完党か぀明確な図を䜜成したす。

Jinja2の仕組み


グロヌバルに、Jinja2は各Python実行可胜テンプレヌトをコンパむルしたす。これはコンテキスト入力を受け取り、文字列レンダリングされたテンプレヌトを返したす。 プロセス党䜓は次のようになりたす。

  1. ダりンロヌドする テンプレヌトをファむルシステム、Pythonパッケヌゞのあるフォルダヌ、メモリに保存するか、オンザフラむで単玔に生成するこずができたす-たず、Jinja2はどのメ゜ッドが関連するかを刀断し、テンプレヌト゜ヌスをメモリにロヌドしたす。
  2. トヌクン化 字句解析噚字句解析噚は、最も単玔な゚ンティティであるトヌクンのテンプレヌトの゜ヌスコヌドを砎りたす。 トヌクンの䟋は、タグを開く構成芁玠{%です。
  3. 解析䞭 。 パヌサヌは、トヌクンのストリヌムを解析し、構文構成を分離したす。 構文コンストラクトの䟋は、 {{ variable }}コンストラクトです。これは、倉数の倀を眮き換えたす3぀のトヌクンで構成されたす- {{ 、 variable名、および}}開きvariable 。
  4. 最適化 。 この段階で、すべおの定数匏が蚈算されたす。 たずえば、構成{{ 1 + 2 }}は{{ 3 }}に倉換されたす。
  5. 䞖代 。 䟝然ずしお抜象構文ツリヌASTずしお栌玍されおいる構文構成䜓は、Pythonコヌドに倉換されたす。
  6. コンパむル 。 結果のPythonコヌドは、組み蟌みのcompile関数によっおコンパむルされたす。 結果のオブゞェクトは、組み蟌み関数execしお起動できたす。テンプレヌトは、レンダリング時にテンプレヌトを実行したす。

Jinja2での拡匵機胜の仕組み


jinja2.ext.Extension拡匵機胜を䜜成するには、 jinja2.ext.Extensionを継承するクラスを定矩する必芁がありたす。 拡匵機胜を有効にするには、環境の䜜成時に拡匵機胜のリストにリストするか、 add_extensionメ゜ッドを䜿甚しお䜜成した埌に远加したす。

千の蚀葉の代わりに簡単なむラスト

 from jinja2 import Environment from jinja2.ext import Extension class MyFirstExtension(Extension): pass class MySecondExtension(Extension): pass environment = Environment(extensions=[MyFirstExtension]) environment.add_extension(MySecondExtension) print(environment.extensions) #  -  # {'__main__.MySecondExtension': <__main__.MySecondExtension object at 0x0000000002FF1780>, '__main__.MyFirstExtension': <__main__.MyFirstExtension object at 0x0000000002FE9BA8>} 

圌らに䜕かをするように教えるこずは残っおいたす このため、おおたかに蚀っお、再定矩できるメ゜ッドは3぀だけです。


さお、順番に始めたしょう。

゜ヌスの読み蟌みを制埡したす


テンプレヌト゜ヌスの盎接読み蟌みを制埡する最も簡単な方法は、独自のロヌダヌを実装するこずです。 これを行うには基本的です jinja2.loaders.BaseLoaderから継承し、 get_source(environment, template_name)メ゜ッドをオヌバヌラむドしたす-完了です。 これは意味のあるこずもありたす。 したがっお、ある日、テンプレヌトの他の郚分ずの埌方互換性のために、テンプレヌトのフォルダ党䜓をテンプレヌトを生成する1぀の゚レガントな関数に眮き換えるこずができる堎合、これらのテンプレヌトがただ存圚するふりをしおブヌトロヌダヌを䜜成するこずができたすそしお、自分でgit rmたす 。

ただし、これはオフトピックです。拡匵機胜はどこにありたすか い぀でも自分が望むものを継承し、そこで必芁だず思うものを倉曎できるこずは明らかです 驚いたこずに、念のため拡匵APIにもテンプレヌトの゜ヌスコヌドを盎接制埡する方法がありたす。

そのため、 Extensionクラスには、ロヌド埌にトヌクン化の前に各テンプレヌトに察しお呌び出されるpreprocessメ゜ッドが含たれおいたす。 眲名は次のようになりたす。

 def preprocess(self, source, name, filename=None): """ : source (String) -    name (String) -   filename (String  None) -   ( ) : String -     """ 

この方法では、必芁なこずは䜕でもできたす。 技術的には、ここのどこかで独自のテンプレヌト蚀語をJinja2テンプレヌトにコンパむルするこずを実装できたす。 しかし...なぜですか おそらく、゜ヌスを盎接倉曎する機胜は、重芁な拡匵機胜を䜜成する際の補助ずしお圹立ちたす。 ただし、Jinja2 APIたたはその実装の機胜に぀いおの知識はここでは必芁ないため、この段階の詳现には觊れず、トヌクン化に進みたせん。

トヌクン化を管理したす


私たちにずっお非垞に興味深いのはfilter_streamメ゜ッドです。このメ゜ッドは、それが開くカスタマむズの豊富な可胜性ず、その神秘的な名前を匕き付けたす。 眲名は次のようになりたす。

 def filter_stream(self, stream): """ : stream (jinja2.lexer.TokenStream) -      : jinja2.lexer.TokenStream -      """ 

䞀般に、Jinja2の字句解析噚ず構文解析噚の盞互䜜甚は次のように線成されおいたす。 字句アナラむザヌ jinja2.lexer.Lexer は、すべおのトヌクンを次々にjinja2.lexer.Tokenするゞェネレヌタヌ jinja2.lexer.Lexer を生成し、このゞェネレヌタヌをjinja2.lexer.TokenStreamオブゞェクトにラップしたす。このオブゞェクトは、ストリヌムをバッファヌし、いく぀かの䟿利な解析メ゜ッドを提䟛したすたずえば、珟圚のトヌクンをストリヌムから匕き出すこずなく衚瀺する機胜。 同様に、拡匵機胜はこのフロヌに圱響を䞎え、メ゜ッドの名前が瀺唆するようにフィルタリングするだけでなく、拡匵するこずもできたす。

Jinja2のトヌクン-オブゞェクトは非垞に単玔です。 本質的に、これらは3぀の名前付きフィヌルドのタプルです


typeフィヌルドのさたざたな定数はjinja2/lexer.py定矩されおいjinja2/lexer.py 

 TOKEN_ADD TOKEN_NE TOKEN_VARIABLE_BEGIN TOKEN_ASSIGN TOKEN_PIPE TOKEN_VARIABLE_END TOKEN_COLON TOKEN_POW TOKEN_RAW_BEGIN TOKEN_COMMA TOKEN_RBRACE TOKEN_RAW_END TOKEN_DIV TOKEN_RBRACKET TOKEN_COMMENT_BEGIN TOKEN_DOT TOKEN_RPAREN TOKEN_COMMENT_END TOKEN_EQ TOKEN_SEMICOLON TOKEN_COMMENT TOKEN_FLOORDIV TOKEN_SUB TOKEN_LINESTATEMENT_BEGIN TOKEN_GT TOKEN_TILDE TOKEN_LINESTATEMENT_END TOKEN_GTEQ TOKEN_WHITESPACE TOKEN_LINECOMMENT_BEGIN TOKEN_LBRACE TOKEN_FLOAT TOKEN_LINECOMMENT_END TOKEN_LBRACKET TOKEN_INTEGER TOKEN_LINECOMMENT TOKEN_LPAREN TOKEN_NAME TOKEN_DATA TOKEN_LT TOKEN_STRING TOKEN_INITIAL TOKEN_LTEQ TOKEN_OPERATOR TOKEN_EOF TOKEN_MOD TOKEN_BLOCK_BEGIN TOKEN_MUL TOKEN_BLOCK_END 

トヌクンを操䜜する䞀般的な拡匵機胜は次のようになりたす。

 from jinja2.ext import Extension from jinja2.lexer import TokenStream class TokensModifyingExtension(Extension): def filter_stream(self, stream): generator = self._generator(stream) return lexer.TokenStream(generator, stream.name, stream.filename) def _generator(self, stream): for token in stream: #        .  . #   -    yield token #   . 

䟋ずしお、倉数のレンダリングのロゞックを倉曎する拡匵機胜を䜜成したしょう。 Jinja2でレンダリングするずきず、 str関数によっお文字列に倉換されたずきずで、オブゞェクトの動䜜を異なるものにしたいずしたす。 オブゞェクトに、テンプレヌトで䜿甚される__jinja__(self)メ゜ッドを定矩するオプションがありたす。 これを行う最も簡単な方法は、 __jinja__メ゜ッドを呌び出すカスタムフィルタヌを远加し、その呌び出しをフォヌム{{ <expression> }}各構成に自動的に眮き換えるこずです。 すべおの拡匵コヌドは次のようになりたす。

 from jinja2 import Environment from jinja2.ext import Extension from jinja2 import lexer class VariablesCustomRenderingExtension(Extension): #    .         # ,       . @staticmethod def _jinja_or_str(obj): try: return obj.__jinja__() except AttributeError: return obj def __init__(self, environment): super(VariablesCustomRenderingExtension, self).__init__(environment) #    .     #      ,   . self._filter_name = "jinja_or_str" environment.filters.setdefault(self._filter_name, self._jinja_or_str) def filter_stream(self, stream): generator = self._generator(stream) return lexer.TokenStream(generator, stream.name, stream.filename) def _generator(self, stream): #     ,     # {{ <expression> }}   {{ (<expression>)|jinja_or_str }} for token in stream: if token.type == lexer.TOKEN_VARIABLE_END: #     {{ <expression> }} -  #   `)|jinja_or_str`. yield lexer.Token(token.lineno, lexer.TOKEN_RPAREN, ")") yield lexer.Token(token.lineno, lexer.TOKEN_PIPE, "|") yield lexer.Token( token.lineno, lexer.TOKEN_NAME, self._filter_name) yield token if token.type == lexer.TOKEN_VARIABLE_BEGIN: #     {{ <expression> }} -  #   `(`. yield lexer.Token(token.lineno, lexer.TOKEN_LPAREN, "(") 

䜿甚䟋

 class Kohai(object): def __jinja__(self): return "senpai rendered me!" if __name__ == "__main__": env = Environment(extensions=[VariablesCustomRenderingExtension]) template = env.from_string("""Kohai says: {{ kohai }}""") print(template.render(kohai=Kohai())) #  "Kohai says: senpai rendered me!". 

Githubですべお芋るこずができたす。

ASTが管理


オヌバヌラむドに䜿甚できる最埌で最も興味深いExtensionクラスメ゜ッドはparseです。

 def parse(self, parser): """ : parse (jinja2.parser.Parser) -    : jinja2.nodes.Stmt  List[jinja2.nodes.Stmt] -  AST,     """ 

これは、拡匵クラスで定矩できるtags属性ず連携しお機胜したす。 この属性には倚くのタグが含たれおいる必芁があり、その凊理は拡匵機胜に委ねられたす。次に䟋を瀺したす。

 class RepeatNTimesExtension(Extension): tags = {"repeat"} 

したがっお、構文解析が、察応するタグの開始を含む構造に到達するず、 parseメ゜ッドが呌び出されたす。

 some text and then {% repeat ... ^ 

この堎合、珟圚凊理䞭のトヌクンを瀺すparser.stream.current属性にはToken(lineno, TOKEN_NAME, "repeat")が含たれたす。

次に、 parseメ゜ッド内で、カスタムタグをparse 、解析結果構文ツリヌの1぀以䞊のノヌドを返す必芁がありたす。 Jinja2では、独自のタむプのノヌドを起動するこずはできたせん。したがっお、組み蟌みのノヌドに満足する必芁がありたす。 幞いなこずに、ほがナニバヌサルなCallBlockノヌドがありたす。これCallBlock以䞋でCallBlockたす。

それたでの間、 For usのような既存のノヌドタむプのロゞックは問題ありたせん。以䞋に、 parseメ゜ッド内で䜿甚するレシピのセットを瀺したす。


必芁なものをすべお解析したら、1぀以䞊のツリヌノヌドを䜜成しお、解析の結果ずしおそれらを返すこずができたす。 Jinja2ノヌドの䜜成に぀いお知っおおくべきこず


このすべおの知識を適甚する䟋ずしお、 {% repeat N times %}...{% endrepeat %}コンストラクトを{% for _ in range(N) %}...{% endfor %}コンストラクト{% for _ in range(N) %}...{% endfor %} 

 from jinja2.ext import Extension from jinja2 import nodes class RepeatNTimesExtension(Extension): #  ,          repeat. #      -  endrepeat,  . tags = {"repeat"} def parse(self, parser): lineno = next(parser.stream).lineno #     . "store" -   ( #   "load",       ). index = nodes.Name("_", "store", lineno=lineno) #    N.       . how_many_times = parser.parse_expression() #   - ,  Jinja2   #  `range(N)`. iterable = nodes.Call( nodes.Name("range", "load"), [how_many_times], [], None, None) #      times. #     ,   . parser.stream.expect("name:times") #      {% endrepeat %}. body = parser.parse_statements(["name:endrepeat"], drop_needle=True) #   for.       #  . return nodes.For(index, iterable, body, [], None, False, lineno=lineno) 

䜿甚䟋

 if __name__ == "__main__": env = Environment(extensions=[RepeatNTimesExtension]) template = env.from_string(u""" {%- repeat 3 times -%} {% if not loop.first and not loop.last %}, {% endif -%} {% if loop.last %}    {% endif -%}  {%- endrepeat -%} """) print(template.render()) #  ",     ". 

Githubですべお芋るこずができたす。

CallBlockを䜿甚する


Jinja2アヌキテクチャは耇雑であるため、構文ツリヌノヌドの新しいクラスを远加できないため、任意の凊理を実行できるナニバヌサルノヌドが必芁です。 そのようなノヌドがあり、これはCallBlockです。

最初に、 {% call %}タグがそれ自䜓でどのように機胜するかを思い出したしょう。 公匏ドキュメントの䟋

 {% macro dump_users(users) -%} <ul> {%- for user in users %} <li><p>{{ user.username|e }}</p>{{ caller(user) }}</li> {%- endfor %} </ul> {%- endmacro %} {% call(user) dump_users(list_of_user) %} <dl> <dl>Realname</dl> <dd>{{ user.realname|e }}</dd> <dl>Description</dl> <dd>{{ user.description }}</dd> </dl> {% endcall %} 

次のこずが起こりたす。

  1. callerずいう名前の䞀時マクロが䜜成されたす。 マクロの本文は、 {% call... %}ず{% endcall %}間のコンテンツです。 マクロは匕数を持぀こずができたす䞊蚘の䟋では、これは1぀のuser匕数ですか、匕数を持たないこずができたす簡易構成{% call something(...) %} 。
  2. call(...)コンストラクトの埌に指定されたマクロがcall(...)たす。 圌はcallerマクロにアクセスでき、おそらくそれを䜿甚したす䜿甚しない堎合もありたす。

ただし、Jinja2のマクロは、文字列を返す関数にすぎたせん。 したがっお、 CallBlockノヌドは、拡匵機胜の腞内のどこかで定矩した関数を同様に䟛絊するこずができたす。

CallBlockを䜿甚しおテキストを凊理する䞀般的な拡匵機胜は、次のようになりたす。

 from jinja2.ext import Extension from jinja2 import nodes class ReplaceTabsWithSpacesExtension(Extension): tags = {"replacetabs"} def parse(self, parser): lineno = next(parser.stream).lineno #  ,  : body = parser.parse_statements( ["name:endreplacetabs"], drop_needle=True) # ! return nodes.CallBlock( self.call_method("_process", [nodes.Const(" ")]), [], [], body, lineno=lineno) def _process(self, replacement, caller): text = caller() return text.replace("\t", replacement) 

どのように機胜したすか


Githubの䜿甚䟋を参照しおください。

最埌に、 CallBlockを䜿甚する拡匵機胜のもう少し耇雑な䟋ず、今日行ったもう1぀のこずは、むンデントフィクサヌです。 テンプレヌトの゜ヌスコヌドず結果の䞡方がむンデントに関しお適切に芋えるように、少なくずもいく぀かの重芁でないテンプレヌトをJinja2で蚘述するこずはほずんど䞍可胜であるこずが知られおいたす。 この誀解を修正するタグを远加しおみたしょう。

 import re from jinja2.ext import Extension from jinja2 import lexer, nodes #       ,  - #   __slots__ = ()   .  , Jinja2 #    - lexer.Token. class RichToken(lexer.Token): pass class AutoindentExtension(Extension): tags = {"autoindent"} #       - #    ? _indent_regex = re.compile(r"^ *") _whitespace_regex = re.compile(r"^\s*$") def _generator(self, stream): #        ,    #    .       (  #  Jinja2). last_line = "" last_indent = 0 for token in stream: if token.type == lexer.TOKEN_DATA: #   - . last_line += token.value if "\n" in last_line: _, last_line = last_line.rsplit("\n", 1) last_indent = self._indent(last_line) #  ^W  . token = RichToken(*token) token.last_indent = last_indent yield token def filter_stream(self, stream): return lexer.TokenStream( self._generator(stream), stream.name, stream.filename) def parse(self, parser): #     autoindent,     , , #   . ,      . last_indent = nodes.Const(parser.stream.current.last_indent) lineno = next(parser.stream).lineno body = parser.parse_statements(["name:endautoindent"], drop_needle=True) #      :) return nodes.CallBlock( self.call_method("_autoindent", [last_indent]), [], [], body, lineno=lineno) def _autoindent(self, last_indent, caller): text = caller() #     ,       #  last_indent.     (, ,  #       ,   ), #       . lines = text.split("\n") if len(lines) < 2: return text first_line, tail_lines = lines[0], lines[1:] min_indent = min( self._indent(line) for line in tail_lines if not self._whitespace_regex.match(line) ) if min_indent <= last_indent: return text dindent = min_indent - last_indent tail = "\n".join(line[dindent:] for line in tail_lines) return "\n".join((first_line, tail)) def _indent(self, string): return len(self._indent_regex.match(string).group()) 

䜿甚䟋

 if __name__ == "__main__": env = Environment(extensions=[AutoindentExtension]) template = env.from_string(u""" {%- autoindent %} {% if True %} What is true, is true. {% endif %} {% if not False %} But what is false, is not true. {% endif %} {% endautoindent -%} """) print(template.render()) #     . 

Githubですべお芋るこずができたす。

独自の拡匵機胜の開発にご関心をお寄せいただきありがずうございたす。

この蚘事で䜿甚されおいるJinjaロゎずそのパヌツの暩利は、Jinjaチヌムに属したす詳现。

Source: https://habr.com/ru/post/J340254/


All Articles