Djangoを読む

はじめに

ここまでビュー(一般的なMVCではController)、モデルと見てきました。チュートリアル3ではビューを作成するにあたりテンプレートを使うという流れになっています。テンプレートはMVC的に言うとView、つまり見た目、HTML生成部分です。

チュートリアルを読んでいくとテンプレートを利用したHTMLコードとして以下の記述があります。

polls/templates/polls/index.html

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

ちなみに、このHTMLには<html>とか<body>がありませんが、別に<html>などが書かれているコードがあってクライアントには全部がまとめられたものが返される、ということはありません。ほんとにこのコードがテンプレートで処理された後そのまま返されます。Railsのようにコントローラと密接に関わりがあって大枠の一部としてレンダリングされるというわけではありません。(大枠を定義したい場合はextendsタグを使います)

テンプレートを利用するビュー関数

polls/views.py

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
 
 
 
 
 
 
 
 
from django.shortcuts import render
 
from .models import Question
 
 
def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

後、テンプレートはsettings.pyのTEMPLATESを参照しているらしいので該当部分

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

それでは見ていきましょう。

django/template/loader.py

スタートはdjango.shortcutsのrender関数ですがチュートリアルにもあるようにこの関数はよく書く処理をまとめているだけです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
 
 
-
|
|
!
 
 
from django.template import loader
 
def render(request, template_name, context=None, content_type=None, status=None, using=None):
    """
    Returns a HttpResponse whose content is filled with the result of calling
    django.template.loader.render_to_string() with the passed arguments.
    """
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)

loaderに進む。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
-
|
|
|
!
 
 
 
 
 
def render_to_string(template_name, context=None, request=None, using=None):
    """
    Loads a template and renders it with a context. Returns a string.
 
    template_name may be a string or a list of strings.
    """
    if isinstance(template_name, (list, tuple)):
        template = select_template(template_name, using=using)
    else:
        template = get_template(template_name, using=using)
    return template.render(context, request)

ここまではrender関数使わない版で書かれている内容、ここからがテンプレートシステムの内部ということになります。

django.template.get_template

get_template関数。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
-
|
|
|
!
 
 
 
 
 
 
 
 
 
def get_template(template_name, using=None):
    """
    Loads and returns a template for the given name.
 
    Raises TemplateDoesNotExist if no such template exists.
    """
    chain = []
    engines = _engine_list(using)
    for engine in engines:
        try:
            return engine.get_template(template_name)
        except TemplateDoesNotExist as e:
            chain.append(e)
 
    raise TemplateDoesNotExist(template_name, chain=chain)

_engine_list関数。usingはNoneなのでallが呼び出されます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
from . import engines
 
def _engine_list(using=None):
    return engines.all() if using is None else [engines[using]]

django.template.engines

enginesは「.」からインポートされているので次に見る先は__init__.pyです。

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
from .utils import EngineHandler
 
engines = EngineHandler()

なかなかしつこい(笑)。utils.pyに行きます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
-
|
|
!
 
 
class EngineHandler(object):
    def __init__(self, templates=None):
        """
        templates is an optional list of template engine definitions
        (structured like settings.TEMPLATES).
        """
        self._templates = templates
        self._engines = {}

ふうむ。allメソッドを見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
 
 
    def all(self):
        return [self[alias] for alias in self]

in self、inで書かれているのですぐ上の__iter__が呼ばれると想像できます。

Everything is expanded.Everything is shortened.
  1
  2
 
 
    def __iter__(self):
        return iter(self.templates)

さらにtemplates。こいつはプロパティです。一部省略して貼り付け

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 
 
 
 
 
 
 
 
 
-
|
!
 
-
!
 
 
 
 
 
 
 
 
 
    @cached_property
    def templates(self):
        if self._templates is None:
            self._templates = settings.TEMPLATES
 
        templates = OrderedDict()
        backend_names = []
        for tpl in self._templates:
            tpl = tpl.copy()
            try:
                # This will raise an exception if 'BACKEND' doesn't exist or
                # isn't a string containing at least one dot.
                default_name = tpl['BACKEND'].rsplit('.', 2)[-2]
            except Exception:
                # 省略
 
            tpl.setdefault('NAME', default_name)
            tpl.setdefault('DIRS', [])
            tpl.setdefault('APP_DIRS', False)
            tpl.setdefault('OPTIONS', {})
 
            templates[tpl['NAME']] = tpl
            backend_names.append(tpl['NAME'])
 
        return templates

デフォルトの設定ではTEMPLATES指定(リスト)には辞書が一つだけ、BACKENDSは'django.template.backends.django.DjangoTemplates'となっています。 rsplitは紛らわしいですが、第2引数の「2」は「最大2回分割(最大3個に分割)」という意味です。つまり、

['django.template.backends', 'django', 'DjangoTemplates']

と分割され、[-2]なのですなわちNAMEは'django'です。

というわけで戻ると、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
 
 
 
    def __iter__(self):
        return iter(self.templates)
 
    def all(self):
        return [self[alias] for alias in self]

self.templatesがOrderedDictなのでイテレータはキーということになります。次に__getitem__を見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 
 
 
 
 
 
-
!
-
|
|
!
 
 
 
 
 
 
    def __getitem__(self, alias):
        try:
            return self._engines[alias]
        except KeyError:
            try:
                params = self.templates[alias]
            except KeyError:
                # 省略
 
            # If importing or initializing the backend raises an exception,
            # self._engines[alias] isn't set and this code may get executed
            # again, so we must preserve the original params. See #24265.
            params = params.copy()
            backend = params.pop('BACKEND')
            engine_cls = import_string(backend)
            engine = engine_cls(params)
 
            self._engines[alias] = engine
            return engine

一回目は_enginesには何も入っていないのでKeyErrorになります。 というわけでBACKENDとして書かれているエンジンがインポートされて返されます。テンプレートタグの読み込みとかしているようですが一旦保留。

django/template/engine.py

さて、というわけでengine、具体的にはDjangoTemplatesオブジェクトがロードされる流れは確認できたので次にテンプレートの取得です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
 
 
 
    def get_template(self, template_name):
        try:
            return Template(self.engine.get_template(template_name), self)
        except TemplateDoesNotExist as exc:
            reraise(exc, self)

ややこしいですが、self.engineはdjango.template.engineモジュールのEngineクラスです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
!
 
 
-
!
 
    def get_template(self, template_name):
        """
        Returns a compiled Template object for the given template name,
        handling template inheritance recursively.
        """
        template, origin = self.find_template(template_name)
        if not hasattr(template, 'render'):
            # template needs to be compiled
            template = Template(template, origin, template_name, engine=self)
        return template

とりあえずfind_templateに進みましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 
 
 
 
 
 
 
 
 
 
 
-
|
!
 
 
 
 
    def find_template(self, name, dirs=None, skip=None):
        tried = []
        for loader in self.template_loaders:
            if loader.supports_recursion:
                try:
                    template = loader.get_template(
                        name, template_dirs=dirs, skip=skip,
                    )
                    return template, template.origin
                except TemplateDoesNotExist as e:
                    tried.extend(e.tried)
            else:
                # RemovedInDjango20Warning: Use old api for non-recursive
                # loaders.
                try:
                    return loader(name, dirs)
                except TemplateDoesNotExist:
                    pass
        raise TemplateDoesNotExist(name, tried=tried)

template_loadersプロパティ

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
    @cached_property
    def template_loaders(self):
        return self.get_template_loaders(self.loaders)

loadersを確認。__init__メソッドに書いてあります。get_template_loadersは結局これらをインポートしてるだけなんで省略。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
        if loaders is None:
            loaders = ['django.template.loaders.filesystem.Loader']
            if app_dirs:
                loaders += ['django.template.loaders.app_directories.Loader']

django.template.loaders

今回使用しているテンプレート的にapp_directoriesのLoaderが使われるだろうからと確認します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
from .filesystem import Loader as FilesystemLoader
 
 
class Loader(FilesystemLoader):

さかのぼる。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
from .base import Loader as BaseLoader
 
 
class Loader(BaseLoader):

baseまで来るとget_templateが書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 
-
|
|
|
|
!
 
 
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
    def get_template(self, template_name, template_dirs=None, skip=None):
        """
        Calls self.get_template_sources() and returns a Template object for
        the first template matching template_name. If skip is provided,
        template origins in skip are ignored. This is used to avoid recursion
        during template extending.
        """
        tried = []
 
        args = [template_name]
        # RemovedInDjango20Warning: Add template_dirs for compatibility with
        # old loaders
        if func_supports_parameter(self.get_template_sources, 'template_dirs'):
            args.append(template_dirs)
 
        for origin in self.get_template_sources(*args):
            if skip is not None and origin in skip:
                tried.append((origin, 'Skipped'))
                continue
 
            try:
                contents = self.get_contents(origin)
            except TemplateDoesNotExist:
                tried.append((origin, 'Source does not exist'))
                continue
            else:
                return Template(
                    contents, origin, origin.template_name, self.engine,
                )
 
        raise TemplateDoesNotExist(template_name, tried=tried)

get_template_sources、get_contentsはいずれもサブクラスのfilesystemのLoaderで定義されています。get_contentsはファイルを読んでるだけなのでget_template_sources

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
-
|
|
|
!
 
 
 
 
 
 
-
|
!
 
 
 
 
 
 
    def get_template_sources(self, template_name, template_dirs=None):
        """
        Return an Origin object pointing to an absolute path in each directory
        in template_dirs. For security reasons, if a path doesn't lie inside
        one of the template_dirs it is excluded from the result set.
        """
        if not template_dirs:
            template_dirs = self.get_dirs()
        for template_dir in template_dirs:
            try:
                name = safe_join(template_dir, template_name)
            except SuspiciousFileOperation:
                # The joined path was located outside of this template_dir
                # (it might be inside another one, so this isn't fatal).
                continue
 
            yield Origin(
                name=name,
                template_name=template_name,
                loader=self,
            )

get_dirsメソッドはapp_directoriesのLoaderではオーバーライドされています。この先は見なくてもいいでしょう。

Everything is expanded.Everything is shortened.
  1
  2
 
 
    def get_dirs(self):
        return get_app_template_dirs('templates')

結果、テンプレートのファイルが読み込まれ、コンパイルが行われます。結構長くなってきたので一旦ここまで。

おわりに

今回はDjangoのテンプレートシステム、とりあえず指定されているテンプレートのファイルを見つけて読み込むまでを見てきました。

感想としては、いろいろなところで同じ名前を使っているな、template、engine、loader、という印象です。委譲という観点では合っているとは思いますがコードを読む側からするとあちこちに目が飛ぶことになる(かつ名前が同じなのでさっき見ていたものとの関連性は?となる)のでややわかりにくく感じました。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-09-24 (日) 23:09:32 (2405d)