
Jinja2は拡張可能で、多くの機能(国際化やループ管理など)が拡張機能として実装されています。 しかし、拡張機能を記述するためのドキュメントは、私には思えますが、やや不完全です。 単純な(ただし慎重にコメントされた)拡張機能の例から、すぐに読むのが非常に難しいいくつかの Jinja2クラスのAPIの説明にジャンプします。 この記事では、この省略を修正し、読者の頭にJinja2の仕組み、拡張機能のしくみ、拡張機能を使用してテンプレート処理のさまざまな段階を変更する方法の完全かつ明確な図を作成します。
グローバルに、Jinja2は各Python実行可能テンプレートをコンパイルします。これはコンテキスト入力を受け取り、文字列(レンダリングされたテンプレート)を返します。 プロセス全体は次のようになります。
- ダウンロードする テンプレートをファイルシステム、Pythonパッケージのあるフォルダー、メモリに保存するか、オンザフライで単純に生成することができます-まず、Jinja2はどのメソッドが関連するかを判断し、テンプレートソースをメモリにロードします。
- トークン化 字句解析器(字句解析器)は、最も単純なエンティティであるトークンのテンプレートのソースコードを破ります。 トークンの例は、タグを開く構成要素
{%
です。 - 解析中 。 パーサーは、トークンのストリームを解析し、構文構成を分離します。 構文コンストラクトの例は、
{{ variable }}
コンストラクトです。これは、変数の値を置き換えます(3つのトークンで構成されます-{{
、variable
名、および}}
開きvariable
)。 - 最適化 。 この段階で、すべての定数式が計算されます。 たとえば、構成
{{ 1 + 2 }}
は{{ 3 }}
に変換されます。 - 世代 。 依然として抽象構文ツリー(AST)として格納されている構文構成体は、Pythonコードに変換されます。
- コンパイル 。 結果のPythonコードは、組み込みの
compile
関数によってコンパイルされます。 結果のオブジェクトは、組み込み関数exec
して起動できます。テンプレートは、レンダリング時にテンプレートを実行します。
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つだけです。
-
preprocess
; -
filter_stream
(その意味は何でも); -
parse
。
さて、順番に始めましょう。
テンプレートソースの直接読み込みを制御する最も簡単な方法は、独自のローダーを実装することです。 これを行うには基本的です:
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つの名前付きフィールドのタプルです:
-
lineno
トークン付きの行番号。 - type-トークンのタイプ。
- value-トークンの文字列値。
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ですべて見ることができます。
オーバーライドに使用できる最後で最も興味深い
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
メソッド内で使用するレシピのセットを示します。
-
lineno = next(parser.stream).lineno
通常、parse
コードの最初の行。next
呼び出しは、タグ名の後の次のトークンにパーサーをシフトし、現在のトークンを返します。 それから行番号だけを覚えています。 ノードの作成時に指定する必要があります。これにより、トレースバックでエラーが発生した場合、そのソース(カスタムタグ)が正しく示されます。 (ノードの作成の詳細は少し下になります。) -
parser.stream.expect(token_description)
現在のトークンが説明に適合する場合は、現在のトークンを返し、次のトークンに移動するか、エラーが発生します。 ここでの説明はtype
トークンのtype
またはタイプ"type:value"
ストリングのいずれかです。 そのため、parser.stream.expect("integer")
は数値を読み取って返そうとするか、クラッシュします。parser.stream.expect("name:in")
キーワードを解析するときにparser.stream.expect("name:in")
使用して、コード内でinキーワードがさらに進んでスキップするようにします。 -
parser.stream.skip_if(token_description)
現在のトークンが説明に適合する場合、True
を返し、次のトークンにスイングします。 そうでない場合はFalse
返します。 典型的な用途は、オプション設計の解析です。 たとえば、すべてが次の解析コードと同じです:
if parser.stream.skip_if('name:if'): test = self.parse_expression()
(はい、Jinja2では、forループにオプションのifサフィックスがあります。) -
expr_node = parser.parse_expression()
式を解析し、対応するASTノードを返すか、クラッシュを試みます。 タグパラメータの解析に使用する価値があります。 上記の例では、この呼び出しを使用してフィルター条件を解析します。 また、expect("name:in")
後にループを使用して、ループの反復可能性を理解します。 -
target_node = parser.parse_assign_target(extra_end_rules=[])
左辺値、つまり、割り当てることができる式またはフォールする式を解析しようとします。 典型的な例は、変数名、コンマで区切られたいくつかの変数名、インデックス付きの式です。 Pythonはタプルの末尾に無料のコンマを許可するため(たとえば、for a, b, c, in []: pass
)、このメソッドは追加のブレーク条件を受け入れることができます(たとえば、ループ変数のリストを解析するforタグはextra_end_rules=["name:in"]
、in
が誤って別の変数として認識されないようにします。
-
body_nodes = parser.parse_statements(end_tokens=[], drop_needle=True)
タグの内部を解析します。parser.stream.current
すでに%}
指している(それ以外の場合はクラッシュする)と想定し、テンプレートはend_tokens
の記述の1つにend_tokens
するファイルまたはトークンの終わりでつまずくまで解析されます。 したがって、ifタグはこのメソッドをend_tokens=["name:elif", "name:else", "name:endif"]
呼び出します。drop_needle=True
パラメーターは、解析後のこの最後のトークンを破棄することを示します。 タグの本文が一方向にしか終わらないと便利です。
必要なものをすべて解析したら、1つ以上のツリーノードを作成して、解析の結果としてそれらを返すことができます。 Jinja2ノードの作成について知っておくべきこと:
- すべてのノードクラスは
jinja2.nodes
で定義され、jinja2.nodes
を継承しjinja2.nodes.Node
。 それらのリストは展開できません。 -
jinja2.nodes.Stmt
を継承するノードのみがparse
から直接返されparse
。 残りは時々機能しますが、すべてを壊す可能性があります。 したがって、次のクラスから選択できます。
Assign ExprStmt Include AssignBlock Extends Macro Block FilterBlock Output Break For Scope CallBlock FromImport ScopedEvalContextModifier Continue If EvalContextModifier Import
-
Node
を継承する各クラスでは、fields
フィールドがフィールドのリストで定義されます。 すべてのフィールドを指定するか、フィールドを指定しないことでノードを作成できます(それらはNone
に初期化され、それらの値は後で指定できます)。 また、キー引数を使用してすべてのノードを作成する場合、lineno
を指定できます。 これを使用して、エラーの場合に適切なトレースバックを取得します。
例:
from jinja2.nodes import * # # () : template_name = Const("lib/stuff.j2") # ( None): inc_node = Include(template_name, False, False, lineno=0) # : inc_node = Include(lineno=0) inc_node.template = template_name inc_node.with_context = False inc_node.ignore_missing = False # , . Jinja2 # ( If - # ) ; # None, , , . # lineno : inc_node = Include() inc_node.lineno = 0
フィールドはキー引数で指定できないことに注意してください:構築
Include(template=template_name, with_context=False, ignore_missing=False)
- 多くのノードフィールドもノードです。 したがって、
Include
は、文字列"lib/stuff.j2"
をテンプレートフィールドとして受け入れることに同意しませんnodes.Const("lib/stuff.j2")
。 どのタイプのフィールドかわからない場合は、jinja2/parser.py
で対応するノードを解析するコードを見つけてjinja2/parser.py
-少なくとも簡単に理解できます(少なくともこの記事を読んだ後は...)。
このすべての知識を適用する例として、
{% 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ですべて見ることができます。
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 %}
次のことが起こります。
-
caller
という名前の一時マクロが作成されます。 マクロの本文は、{% call... %}
と{% endcall %}
間のコンテンツです。 マクロは引数を持つことができます(上記の例では、これは1つのuser
引数です)か、引数を持たないことができます(簡易構成{% call something(...) %}
)。 -
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)
どのように機能しますか?
-
call_method
は、Jinja2ノードのクラスメソッドへの呼び出しをラップするExtension
クラスの特別なメソッドです。 結果はパラメーターとしてCallBlock
式を期待する場所、特にCallBlock
関数呼び出しを期待する場所に渡すことができます。 -
CodeBlock
のparse
CodeBlock
から戻るときが来ると、ReplaceTabsWithSpacesExtension._process
メソッドを呼び出します。 最初に、call_method
呼び出し中に指定された引数が渡され(この場合、1つの引数は4つのスペースの文字列です)、次に同じcaller
が渡されます。これは単にJinja2マクロであり、単に文字列を取得するために呼び出されます。 -
caller
マクロを引数で呼び出す必要がある場合、CodeBlock
ノードのフィールドにリストする必要があります(この例では空のリストがあります)。
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チームに属します(詳細)。