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チームに属します詳細)。



All Articles