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