[[Djangoを読む]]
 
 #contents
 
 *はじめに [#gdef0168]
 
 ここまでビュー(一般的なMVCではController)、モデルと見てきました。[[チュートリアル3>https://docs.djangoproject.com/ja/1.10/intro/tutorial03/]]ではビューを作成するにあたりテンプレートを使うという流れになっています。テンプレートは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
 #code(Python){{
 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を参照しているらしいので該当部分
 
 #code(Python){{
 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 [#v7413407]
 
 スタートはdjango.shortcutsのrender関数ですがチュートリアルにもあるようにこの関数はよく書く処理をまとめているだけです。
 
 #code(Python){{
 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に進む。
 
 #code(Python){{
 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 [#y62b519c]
 
 get_template関数。
 
 #code(Python){{
 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が呼び出されます。
 
 #code(Python){{
 from . import engines
 
 def _engine_list(using=None):
     return engines.all() if using is None else [engines[using]]
 }}
 
 ***django.template.engines [#gbd51587]
 
 enginesは「.」からインポートされているので次に見る先は__init__.pyです。
 
 #code(Python){{
 from .utils import EngineHandler
 
 engines = EngineHandler()
 }}
 
 なかなかしつこい(笑)。utils.pyに行きます。
 
 #code(Python){{
 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メソッドを見てみましょう。
 
 #code(Python){{
     def all(self):
         return [self[alias] for alias in self]
 }}
 
 in self、inで書かれているのですぐ上の__iter__が呼ばれると想像できます。
 
 #code(Python){{
     def __iter__(self):
         return iter(self.templates)
 }}
 
 さらにtemplates。こいつはプロパティです。一部省略して貼り付け
 
 #code(Python){{
     @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'です。
 
 というわけで戻ると、
 
 #code(Python){{
     def __iter__(self):
         return iter(self.templates)
 
     def all(self):
         return [self[alias] for alias in self]
 }}
 
 self.templatesがOrderedDictなのでイテレータはキーということになります。次に__getitem__を見てみましょう。
 
 #code(Python){{
     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 [#v08cb7f2]
 
 さて、というわけでengine、具体的にはDjangoTemplatesオブジェクトがロードされる流れは確認できたので次にテンプレートの取得です。
 
 #code(Python){{
     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クラスです。
 
 #code(Python){{
     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に進みましょう。
 
 #code(Python){{
     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プロパティ
 
 #code(Python){{
     @cached_property
     def template_loaders(self):
         return self.get_template_loaders(self.loaders)
 }}
 
 loadersを確認。__init__メソッドに書いてあります。get_template_loadersは結局これらをインポートしてるだけなんで省略。
 
 #code(Python){{
         if loaders is None:
             loaders = ['django.template.loaders.filesystem.Loader']
             if app_dirs:
                 loaders += ['django.template.loaders.app_directories.Loader']
 }}
 
 ***django.template.loaders [#y0aa1341]
 
 今回使用しているテンプレート的にapp_directoriesのLoaderが使われるだろうからと確認します。
 
 #code(Python){{
 from .filesystem import Loader as FilesystemLoader
 
 
 class Loader(FilesystemLoader):
 }}
 
 さかのぼる。
 
 #code(Python){{
 from .base import Loader as BaseLoader
 
 
 class Loader(BaseLoader):
 }}
 
 baseまで来るとget_templateが書かれています。
 
 #code(Python){{
     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
 
 #code(Python){{
     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ではオーバーライドされています。この先は見なくてもいいでしょう。
 
 #code(Python){{
     def get_dirs(self):
         return get_app_template_dirs('templates')
 }}
 
 結果、テンプレートのファイルが読み込まれ、コンパイルが行われます。結構長くなってきたので一旦ここまで。
 
 *おわりに [#t93b2228]
 
 今回はDjangoのテンプレートシステム、とりあえず指定されているテンプレートのファイルを見つけて読み込むまでを見てきました。
 
 感想としては、いろいろなところで同じ名前を使っているな、template、engine、loader、という印象です。委譲という観点では合っているとは思いますがコードを読む側からするとあちこちに目が飛ぶことになる(かつ名前が同じなのでさっき見ていたものとの関連性は?となる)のでややわかりにくく感じました。
 

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS