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
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を参照しているらしいので該当部分
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関数ですがチュートリアルにもあるようにこの関数はよく書く処理をまとめているだけです。
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に進む。
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関数。
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が呼び出されます。
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です。
1
2
3
|
| from .utils import EngineHandler
engines = EngineHandler()
|
なかなかしつこい(笑)。utils.pyに行きます。
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メソッドを見てみましょう。
1
2
|
| def all(self):
return [self[alias] for alias in self]
|
in self、inで書かれているのですぐ上の__iter__が呼ばれると想像できます。
1
2
|
| def __iter__(self):
return iter(self.templates)
|
さらにtemplates。こいつはプロパティです。一部省略して貼り付け
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:
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'です。
というわけで戻ると、
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__を見てみましょう。
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:
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オブジェクトがロードされる流れは確認できたので次にテンプレートの取得です。
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クラスです。
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 = Template(template, origin, template_name, engine=self)
return template
|
とりあえずfind_templateに進みましょう。
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:
try:
return loader(name, dirs)
except TemplateDoesNotExist:
pass
raise TemplateDoesNotExist(name, tried=tried)
|
template_loadersプロパティ
1
2
3
|
| @cached_property
def template_loaders(self):
return self.get_template_loaders(self.loaders)
|
loadersを確認。__init__メソッドに書いてあります。get_template_loadersは結局これらをインポートしてるだけなんで省略。
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が使われるだろうからと確認します。
1
2
3
4
|
| from .filesystem import Loader as FilesystemLoader
class Loader(FilesystemLoader):
|
さかのぼる。
1
2
3
4
|
| from .base import Loader as BaseLoader
class Loader(BaseLoader):
|
baseまで来るとget_templateが書かれています。
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]
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
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:
continue
yield Origin(
name=name,
template_name=template_name,
loader=self,
)
|
get_dirsメソッドはapp_directoriesのLoaderではオーバーライドされています。この先は見なくてもいいでしょう。
1
2
|
| def get_dirs(self):
return get_app_template_dirs('templates')
|
結果、テンプレートのファイルが読み込まれ、コンパイルが行われます。結構長くなってきたので一旦ここまで。
おわりに †
今回はDjangoのテンプレートシステム、とりあえず指定されているテンプレートのファイルを見つけて読み込むまでを見てきました。
感想としては、いろいろなところで同じ名前を使っているな、template、engine、loader、という印象です。委譲という観点では合っているとは思いますがコードを読む側からするとあちこちに目が飛ぶことになる(かつ名前が同じなのでさっき見ていたものとの関連性は?となる)のでややわかりにくく感じました。