Jinjaのコードをがっつり読み始めてみた①


ひらう子です。

昨年2021年に開発エンジニアとして就職しまして、それまで個人開発ばかりやっていた頃に比べると、自分以外の人の書いたコードを読む機会がかなり増えました。また、個人で使っていてもいまいちピンとこなかったGitの扱いにも少し慣れてきました。
業務でコードを読むこと自体は良いのですが、就職して初めて触れるシステムのコードを読んでいても「納得できた」という達成感が中々得られません。どうもこのままでは自信もつきづらい感じがしますし、精神的な疲労感が強いです。
そこで、半ば趣味的な目的で、業務上の必要事項として読むことになるコードとは別に、自分の慣れ親しんだシステムのコードを読んで理解を深めていくことにしました。

Jinjaを選んだ理由

コードリーディングの題材としてJinjaを選んだ要因としては、Pythonであること、自分が使ったことがあるシステムであること、ある程度の人数の人が携わっているプロジェクトであることが挙げられます。

私は個人開発ではPythonを使ってきていますので、Pythonのプロジェクトであれば文法的に未知の領域が少なく、コードリーディングの過程で自分でコードを書いて動きを検証することも容易です。

また、業務で自分があまり触れたことのないシステムに携わっている中で、自信をつけるべく敢えてコードリーディングによる自習を行うという話なので、対象が慣れ親しんだプロジェクトであることは前提です。

そして、ある程度の人数が携わっているプロジェクトであれば、「より良いコーディング手法」に出会える機会は増えることが考えられます。

その上で、私が以前にHTMLSeaというAltHTMLを趣味で個人開発していた際に、パーサの制作が中々楽しかったことから、テンプレート言語を処理するJinjaのコードを読んでみるということにしました。JinjaはGithubでもContributorが250人を超えているプロジェクトですので、携わっている人数としては充分でしょう。
今後、そのHTMLSeaをテンプレート言語として使えるように出来ればいいなと思っているので、その上でも役に立つ知識をつけられればと期待しています。

とにかくコードをがっつりと読むのは初めてですし、かなり自己流でやっていくことになると思います。そんな訳で、どんな感じで読解を進めていったのかの記録をゆるゆると記事にしていければと思います。

まずはコードのファイル構成とクラスを概観してみる

まずは、JinjaのGithubリポジトリを見てみます。今回コードリーディングの対象とするのは、現時点で最新版の3.0.3ということにします。リポジトリをgitで手元でクローンしてきます。
https://github.com/pallets/jinja/tree/3.0.3

この中で、実際にパッケージとして読み込まれるのはsrc/jinja2のディレクトリです。ディレクトリ内のpythonスクリプトの行数は、以下の通りです。

$ ls src/jinja2/* | xargs wc -l
    37 src/jinja2/__init__.py
     6 src/jinja2/_identifier.py
    75 src/jinja2/async_utils.py
   364 src/jinja2/bccache.py
  1957 src/jinja2/compiler.py
    20 src/jinja2/constants.py
   191 src/jinja2/debug.py
    48 src/jinja2/defaults.py
  1655 src/jinja2/environment.py
   166 src/jinja2/exceptions.py
   859 src/jinja2/ext.py
  1771 src/jinja2/filters.py
   318 src/jinja2/idtracking.py
   869 src/jinja2/lexer.py
   652 src/jinja2/loaders.py
   111 src/jinja2/meta.py
   130 src/jinja2/nativetypes.py
  1204 src/jinja2/nodes.py
    47 src/jinja2/optimizer.py
  1040 src/jinja2/parser.py
     0 src/jinja2/py.typed
  1053 src/jinja2/runtime.py
   428 src/jinja2/sandbox.py
   255 src/jinja2/tests.py
   755 src/jinja2/utils.py
    92 src/jinja2/visitor.py
 14103 total

行数で見た限り、compiler.py(1957行)、filters.py(1771行)、environment.py(1655行)、nodes.py(1204行)、runtime.py(1053行)、parser.py(1040行)、ext.py(859行)、lexer.py(869行)あたりが大きなファイルという感じですね。ファイル名で何が定義されているファイルなのかは大まかに推測できる気がします。

$ grep class src/jinja2コマンドで各ファイルの中のクラス定義を探してみます。それをファイルに書き出して、明らかにクラス定義ではない行を削除すると、それぞれのファイルで定義されているクラス定義は以下のような感じでした。

src/jinja2/utils.py:class _PassArg(enum.Enum):
src/jinja2/utils.py:class LRUCache:
src/jinja2/utils.py:class Cycler:
src/jinja2/utils.py:class Joiner:
src/jinja2/utils.py:class Namespace:
src/jinja2/visitor.py:    class VisitCallable(te.Protocol):
src/jinja2/visitor.py:class NodeVisitor:
src/jinja2/visitor.py:class NodeTransformer(NodeVisitor):
src/jinja2/environment.py:class Environment:
src/jinja2/environment.py:class Template:
src/jinja2/environment.py:class TemplateModule:
src/jinja2/environment.py:class TemplateExpression:
src/jinja2/environment.py:class TemplateStream:
src/jinja2/exceptions.py:class TemplateError(Exception):
src/jinja2/exceptions.py:class TemplateNotFound(IOError, LookupError, TemplateError):
src/jinja2/exceptions.py:class TemplatesNotFound(TemplateNotFound):
src/jinja2/exceptions.py:class TemplateSyntaxError(TemplateError):
src/jinja2/exceptions.py:class TemplateAssertionError(TemplateSyntaxError):
src/jinja2/exceptions.py:class TemplateRuntimeError(TemplateError):
src/jinja2/exceptions.py:class UndefinedError(TemplateRuntimeError):
src/jinja2/exceptions.py:class SecurityError(TemplateRuntimeError):
src/jinja2/exceptions.py:class FilterArgumentError(TemplateRuntimeError):
src/jinja2/nativetypes.py:class NativeCodeGenerator(CodeGenerator):
src/jinja2/nativetypes.py:class NativeEnvironment(Environment):
src/jinja2/nativetypes.py:class NativeTemplate(Template):
src/jinja2/lexer.py:class Failure:
src/jinja2/lexer.py:class Token(t.NamedTuple):
src/jinja2/lexer.py:class TokenStreamIterator:
src/jinja2/lexer.py:class TokenStream:
src/jinja2/lexer.py:class OptionalLStrip(tuple):
src/jinja2/lexer.py:class _Rule(t.NamedTuple):
src/jinja2/lexer.py:class Lexer:
src/jinja2/loaders.py:class BaseLoader:
src/jinja2/loaders.py:class FileSystemLoader(BaseLoader):
src/jinja2/loaders.py:class PackageLoader(BaseLoader):
src/jinja2/loaders.py:class DictLoader(BaseLoader):
src/jinja2/loaders.py:class FunctionLoader(BaseLoader):
src/jinja2/loaders.py:class PrefixLoader(BaseLoader):
src/jinja2/loaders.py:class ChoiceLoader(BaseLoader):
src/jinja2/loaders.py:class _TemplateModule(ModuleType):
src/jinja2/loaders.py:class ModuleLoader(BaseLoader):
src/jinja2/nodes.py:class Impossible(Exception):
src/jinja2/nodes.py:class NodeType(type):
src/jinja2/nodes.py:class EvalContext:
src/jinja2/nodes.py:class Node(metaclass=NodeType):
src/jinja2/nodes.py:class Stmt(Node):
src/jinja2/nodes.py:class Helper(Node):
src/jinja2/nodes.py:class Template(Node):
src/jinja2/nodes.py:class Output(Stmt):
src/jinja2/nodes.py:class Extends(Stmt):
src/jinja2/nodes.py:class For(Stmt):
src/jinja2/nodes.py:class If(Stmt):
src/jinja2/nodes.py:class Macro(Stmt):
src/jinja2/nodes.py:class CallBlock(Stmt):
src/jinja2/nodes.py:class FilterBlock(Stmt):
src/jinja2/nodes.py:class With(Stmt):
src/jinja2/nodes.py:class Block(Stmt):
src/jinja2/nodes.py:class Include(Stmt):
src/jinja2/nodes.py:class Import(Stmt):
src/jinja2/nodes.py:class FromImport(Stmt):
src/jinja2/nodes.py:class ExprStmt(Stmt):
src/jinja2/nodes.py:class Assign(Stmt):
src/jinja2/nodes.py:class AssignBlock(Stmt):
src/jinja2/nodes.py:class Expr(Node):
src/jinja2/nodes.py:class BinExpr(Expr):
src/jinja2/nodes.py:class UnaryExpr(Expr):
src/jinja2/nodes.py:class Name(Expr):
src/jinja2/nodes.py:class NSRef(Expr):
src/jinja2/nodes.py:class Literal(Expr):
src/jinja2/nodes.py:class Const(Literal):
src/jinja2/nodes.py:class TemplateData(Literal):
src/jinja2/nodes.py:class Tuple(Literal):
src/jinja2/nodes.py:class List(Literal):
src/jinja2/nodes.py:class Dict(Literal):
src/jinja2/nodes.py:class Pair(Helper):
src/jinja2/nodes.py:class Keyword(Helper):
src/jinja2/nodes.py:class CondExpr(Expr):
src/jinja2/nodes.py:class _FilterTestCommon(Expr):
src/jinja2/nodes.py:class Filter(_FilterTestCommon):
src/jinja2/nodes.py:class Test(_FilterTestCommon):
src/jinja2/nodes.py:class Call(Expr):
src/jinja2/nodes.py:class Getitem(Expr):
src/jinja2/nodes.py:class Getattr(Expr):
src/jinja2/nodes.py:class Slice(Expr):
src/jinja2/nodes.py:class Concat(Expr):
src/jinja2/nodes.py:class Compare(Expr):
src/jinja2/nodes.py:class Operand(Helper):
src/jinja2/nodes.py:class Mul(BinExpr):
src/jinja2/nodes.py:class Div(BinExpr):
src/jinja2/nodes.py:class FloorDiv(BinExpr):
src/jinja2/nodes.py:class Add(BinExpr):
src/jinja2/nodes.py:class Sub(BinExpr):
src/jinja2/nodes.py:class Mod(BinExpr):
src/jinja2/nodes.py:class Pow(BinExpr):
src/jinja2/nodes.py:class And(BinExpr):
src/jinja2/nodes.py:class Or(BinExpr):
src/jinja2/nodes.py:class Not(UnaryExpr):
src/jinja2/nodes.py:class Neg(UnaryExpr):
src/jinja2/nodes.py:class Pos(UnaryExpr):
src/jinja2/nodes.py:class EnvironmentAttribute(Expr):
src/jinja2/nodes.py:class ExtensionAttribute(Expr):
src/jinja2/nodes.py:class ImportedName(Expr):
src/jinja2/nodes.py:class InternalName(Expr):
src/jinja2/nodes.py:class MarkSafe(Expr):
src/jinja2/nodes.py:class MarkSafeIfAutoescape(Expr):
src/jinja2/nodes.py:class ContextReference(Expr):
src/jinja2/nodes.py:class DerivedContextReference(Expr):
src/jinja2/nodes.py:class Continue(Stmt):
src/jinja2/nodes.py:class Break(Stmt):
src/jinja2/nodes.py:class Scope(Stmt):
src/jinja2/nodes.py:class OverlayScope(Stmt):
src/jinja2/nodes.py:class EvalContextModifier(Stmt):
src/jinja2/nodes.py:class ScopedEvalContextModifier(EvalContextModifier):
src/jinja2/idtracking.py:class Symbols:
src/jinja2/idtracking.py:class RootVisitor(NodeVisitor):
src/jinja2/idtracking.py:class FrameSymbolVisitor(NodeVisitor):
src/jinja2/runtime.py:    class LoopRenderFunc(te.Protocol):
src/jinja2/runtime.py:class TemplateReference:
src/jinja2/runtime.py:class Context:
src/jinja2/runtime.py:class BlockReference:
src/jinja2/runtime.py:class LoopContext:
src/jinja2/runtime.py:class AsyncLoopContext(LoopContext):
src/jinja2/runtime.py:class Macro:
src/jinja2/runtime.py:class Undefined:
src/jinja2/runtime.py:    class LoggingUndefined(base):  # type: ignore
src/jinja2/runtime.py:class ChainableUndefined(Undefined):
src/jinja2/runtime.py:class DebugUndefined(Undefined):
src/jinja2/runtime.py:class StrictUndefined(Undefined):
src/jinja2/ext.py:    class _TranslationsBasic(te.Protocol):
src/jinja2/ext.py:    class _TranslationsContext(_TranslationsBasic):
src/jinja2/ext.py:class Extension:
src/jinja2/ext.py:class InternationalizationExtension(Extension):
src/jinja2/ext.py:class ExprStmtExtension(Extension):
src/jinja2/ext.py:class LoopControlExtension(Extension):
src/jinja2/ext.py:class DebugExtension(Extension):
src/jinja2/ext.py:class _CommentFinder:
src/jinja2/meta.py:class TrackingCodeGenerator(CodeGenerator):
src/jinja2/bccache.py:    class _MemcachedClient(te.Protocol):
src/jinja2/bccache.py:class Bucket:
src/jinja2/bccache.py:class BytecodeCache:
src/jinja2/bccache.py:        class MyCache(BytecodeCache):
src/jinja2/bccache.py:class FileSystemBytecodeCache(BytecodeCache):
src/jinja2/bccache.py:class MemcachedBytecodeCache(BytecodeCache):
src/jinja2/parser.py:class Parser:
src/jinja2/optimizer.py:class Optimizer(NodeTransformer):
src/jinja2/compiler.py:class MacroRef:
src/jinja2/compiler.py:class Frame:
src/jinja2/compiler.py:class VisitorExit(RuntimeError):
src/jinja2/compiler.py:class DependencyFinderVisitor(NodeVisitor):
src/jinja2/compiler.py:class UndeclaredNameVisitor(NodeVisitor):
src/jinja2/compiler.py:class CompilerExit(Exception):
src/jinja2/compiler.py:class CodeGenerator(NodeVisitor):
src/jinja2/compiler.py:    class _FinalizeInfo(t.NamedTuple):
src/jinja2/sandbox.py:class SandboxedEnvironment(Environment):
src/jinja2/sandbox.py:class ImmutableSandboxedEnvironment(SandboxedEnvironment):
src/jinja2/sandbox.py:class SandboxedFormatter(Formatter):
src/jinja2/sandbox.py:class SandboxedEscapeFormatter(SandboxedFormatter, EscapeFormatter):
src/jinja2/filters.py:    class HasHTML(te.Protocol):
src/jinja2/filters.py:class _GroupTuple(t.NamedTuple):

クラスは159件あるようです。Jinjaのコードを理解していく上では、この159件のクラスと、Jinjaが依存しているパッケージの仕様を理解していくのが正道のように思われます。継承関係のあるクラスも多いので、まずは抽象的なクラスの把握に努めるのがよいのかなと思います。

クラス定義がインデントされている部分がいくつかあります。実際にコードを見たところ、if文による場合分けの中でクラス定義されているものや、クラス内で定義されている内部クラス、クラス自体を返す関数の中のクラス定義部分というケースがありました。後ほど、しっかり見ていきましょう。

ひとまずここまでの感想と課題

Jinjaはflaskで標準採用されていることから、いままでJinjaのテンプレートを書くことはとても多かったのですが、あくまで使う側として触れる程度で、コードに触れるという事はあまりやっておりませんでした。今回クラス数を見たところ159件ということなので、まずはある程度時間をかけてそれぞれのクラスの役割を把握していくのが重要なのかなと思っております。

また、いままでJinjaテンプレートを書く際には漫然と書いているだけで、あまりJinjaのドキュメントを読み込んでいなかったことにふと気づきました。

ひとまず、次の課題として、サイズの大きなファイルからスタートしてクラス同士の依存関係を把握し全体の構造を理解していくということ、ドキュメントを繰り返し読み込むということに取り組んでいこうと思っています。