4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Djangoのサーバーがリクエストを受け取ってからの流れ ②

Last updated at Posted at 2021-06-02

前回までの流れ

①. リクエストを受信
 ↓
②. ミドルウェアチェーンの作成
 ↓
③. ミドルウェアチェーンにリクエストを渡す(ここまで)

前回の記事はこちら。
Djangoのサーバーがリクエストを受け取ってからの流れ ①

前回は、ミドルウェアチェーンにリクエストを渡すところまでを追ったので、今回は_get_responseメソッドが動くところから追ってみようと思います。この辺からURLResolverなどが出てきます。

今回やる事

①. ミドルウェアチェーンに含まれる、_get_responseメソッドが呼び出される
 ↓
②. 該当Viewのメソッドを呼び出す事により、レスポンスクラスの取得
 ↓
③. テンプレートエンジンによりテンプレートのレンダリング
この間にミドルウェアによるprocess_responseも呼び出される。
 ↓
④. レンダリングされたレスポンスクラスをストリームに書き込む
 ↓
クライアントに返る。
までを追っていきたいと思います!

※レンダリング処理は一部省きます。

実装を追っていく

_get_responseメソッド

django.core.handlers.base.py
class BaseHandler:

    def _get_response(self, request):
        response = None

        # 1. resolve_requestメソッドによって該当メソッド、引数取得
        callback, callback_args, callback_kwargs = self.resolve_request(request) 


        # viewミドルウェアを通したり...
        

        if response is None:
            # 2. viewに対してトランザクション関連のチェック
            wrapped_callback = self.make_view_atomic(callback)
                        

            # 非同期のviewだったらサブスレッドで動かしたり...


            try:
         # 3. 該当メソッドにリクエストと引数を渡し、レスポンスを取得する。
                response = wrapped_callback(request, *callback_args, **callback_kwargs)
            except Exception as e:
                # exceptionミドルウェアを通す
                response = self.process_exception_by_middleware(e, request)
                if response is None:
                    raise


        # エラーチェックとか...
        

        if hasattr(response, 'render') and callable(response.render): # 4
            # templateミドルウェアを通したり...

            try:
                # renderが実装されてたら、テンプレートをレンダリングし、テンプレートレスポンスを取得する。
                response = response.render()
            except Exception as e:
                # exceptionミドルウェアを通す
                response = self.process_exception_by_middleware(e, request)
                if response is None:
                    raise

        # 取得したレスポンスを返す。
        return response # 4

_get_responseメソッドでは、大まかに以下の様な処理が実装されているようです。

  1. resolve_requestで該当View、引数取得
  2. make_view_atomicでトランザクションチェック
  3. 該当viewにリクエストと引数を渡して呼び出す事でレスポンスを取得する
  4. レスポンスを返す(renderが実装されていればテンプレートレスポンスを返す

ここらへんは1年くらい前に追ったのですが、1年前に比べると色々とソースが変わっていてびっくりしました。オープンソースだしそりゃそうですね。

では、1から追っていきます。

①. resolve_requestで該当Viewらの取得

django.core.handlers.base.py
class BaseHandler:

    def resolve_request(self, request):
        if hasattr(request, 'urfconf'):
            urlconf = request.urlconf
            set_urlconf(urlconf)
            resolver = get_resolver(urlconf)
        else:
            resolver = get_resolver()

        resolver_match = resolver.resolve(request.path_info)
        request.resolver_match = resolver_match
        return resolver_match

ここではリクエストがurlconfを持っていたら、その引数をget_resolverメソッドに渡してURLResolverを取得しています。
※urlconfがリクエストに無くても後でsettings.pyに記載されているROOT_URLCONFをurlconfとして設定します。

そして、URLResolverのresolveメソッドにより、ResolverMatchクラスを返しています。
このResolverMatchクラスには__getitem__メソッドが実装されており、このクラスからViewの該当メソッド、該当引数が取得出来るようです。

get_resolver

urlconfを設定し、URLResolverを取得するメソッドです。
URLResolverインスタンス化時には、patternにRegexPattern, urlconf_nameにurlconfを設定しています。あとで、RegexPatternのmatchメソッドにより、まず最初のURLの探索が始まります。

django.urls.resolvers.py
def get_resolver(urlconf=None):
    if urlconf is None:
        urlconf = settings.ROOT_URLCONF
    return _get_cached_resolver(urlconf)

@functools.lru_cache(maxsize=None)
def _get_cached_resolver(urlconf=None):
    return URLResolver(RegexPattern(r'^/'), urlconf))


class URLResolver:
    # 長いので省略
    def __init___(self, pattern, urlconf_name ... ):
        self.pattern = pattern
        self.urlconf_name = urlconf_name

functools.lru_cacheというのは、LRUという仮想メモリのページングアルゴリズムを使用しキャッシュ化出来るメソッドです。
このメソッドにより、キャッシュからURLResolverを取得しているようです。

LRU(Least Recently Used)

メモリ中で今後使われる可能性が少ないページをHD(仮想メモリ)に退避させ実メモリを空ける機能があり、これを仮想メモリと言う。
LRUは、最後に利用してから最も時間が経過しているページを仮想メモリに退避させる。

以下ドキュメントから。

If maxsize is set to None, the LRU feature is disabled and the cache can grow without bound

しかし、上記の通り、ここではmaxsizeがNoneと指定されているため、LRU機能は無効になりキャッシュは無制限に拡大される様子。

URLResolverクラス

まず最初にRegexPatternのmatchメソッドにより探索が行われ、その結果一致するグループがあれば、一致するパスの検索に入りViewを取得しに行きます。

流れとしては、ユーザーが定義したurlpatternsをループし、該当のパスが存在するか検索します。該当のものがあれば、ResolverMatchクラスに諸々情報を詰め、returnする感じです。

RegexPatternから見ていきます。

まず↑でRegexPattern(r'^/')という風にインスタンス化しました。それはself._regexとして保持されます。

django.urls.resolvers.py
class RegexPattern(CheckURLMixin):
    regex = LocaleRegexDescriptor('_regex')

    def __init__(self, regex, name=None, is_endpoint=False):
        self._regex = regex
        self._regex_dict = {}
        self._is_endpoint = is_endpoint
        self.name = name
        self.converters = {}

そしてself.regexであるLocaleRegexDescriptorのsearchメソッドによって正規表現で探索を行います。

django.urls.resolvers.py
class LocaleRegexDescriptor:
    def __init__(self, attr):
        # attrには_regexという文字列が入る。
        self.attr = attr

    def __get__(self, instance, cls=None):
        ...
        # RegexPatternの_regex ^/ をpatternに代入する。この場合、リクエストのpath_infoがスラッシュ始まりかどうか判定する。
        pattern = getattr(instance, self.attr)
        if isinstance(pattern, str):
            instance.__dict__['regex'] = instance._compile(pattern)
            # そして、RegexPatternの_compileメソッドが返る。
            return instance.__dict__['regex']
        ...

class RegexPattern(CheckURLMixin):
    ...    
    def match(self, path):
        # これにより、self.regexの__get__メソッドが動いて、self._compileメソッドにより返されたものに対してsearchを行う。
        match = self.regex.search(path)

        if match:
            kwargs = match.groupdict()
            args = () if kwargs else match.groups()
            kwargs = {k: v for k, v in kwargs.items() if v is not None}
            # ここでマッチした場所の終わり移行のパスを返すようにしている。
            return path[match.end):], args, kwargs
        return None

    def _compile(self, regex):
        try:
            return re.compile(regex)
        ...

まとめると、リクエストにpath_infoが含まれていた場合は最初のスラッシュ以降のパスを返す。path_infoが含まれていない場合でも、/がWSGIRequestのインスタンス化時に設定されているので、matchしたと判定される。

path_infoについて公式ドキュメントから引用すると、

HttpRequest.path_info

Under some Web server configurations, the portion of the URL after the host name is split up into a script
prefix portion and a path info portion. The path_info attribute always contains the path info portion of the
path, no matter what Web server is being used. Using this instead of path can make your code easier to move
between test and deployment servers.
For example, if the WSGIScriptAlias for your application is set to "/minfo",
then path might be "/minfo/music/bands/the_beatles/" and path_info would be "/music/bands/the_beatles/".

てな感じみたいです。

RegexPatternのmatchメソッドがなんとなく掴めたところで、次の処理を見ていきます。

django.urls.resolvers.py
class URLResolver:
    ...
    def resolve(self, path):
        path = str(path)
        tried = []
        # このmatchメソッドが↑でやったもの。パスとargs, kwargsが分けられる。
        # ここはpath_infoが変なものでない限り必ず通るっぽい。
        match = self.pattern.match(path)
        if match:
            new_path, args, kwargs = match

            for pattern in self.url_patterns: # urls.pyのurlpatternsのループ
                try:
                    sub_match = pattern.resolve(new_path)
                except Resolver404 as e:
                    self._extend_tried(tried, pattern, e.args[0].get('tried'))
                else:
                    if sub_match:
                        sub_match_dict = {**kwargs, **self.default_kwargs}
                        sub_match_dict.update(sub_match.kwargs)
                        sub_match_args = sub_match.args
                        if not sub_match_dict:
                            sub_match_args = args + sub_match.args
                        ...
                        return ResolverMatch(
                            sub_match.func,
                            sub_match_args,
                            sub_match_dict,
                            sub_match.url_name,
                            # 省略
                        )
                    ...

urls.pyのurlpatternsのループ

例として適当なListViewを用意しました。

view.py
class IndexView(generics.ListView):
    template_name = 'index.html'
    model = Test
urls.py
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
]

このurlpatternsがループされていき、個々のpathのresolveメソッドが走るようです。

※includeで指定した場合は、URLResolverのresolverが再度走る事によって、指定したURLもちゃんと検索されるようです。

urls.pyを記載する際は、Djangoだったら今は大体pathで書くと思いますが、このpathというのは何なのか。

pathの実装

以下のソースから、
pathの正体はどうやらURLResolverクラスか、URLPatternクラスになっているようです。

django.urls.conf.py
path = partial(_path, Pattern=RoutePattern)

def _path(route, view, kwargs=None, name=None, Pattern=None):
    if isinstance(view, (list, tuple)):
        pattern = Pattern(route, is_endpoint=False)
        urlconf_module, app_name, namespace = view
        return URLResolver(
            pattern,
            urlconf_module,
            kwargs,
            app_name=app_name,
            namespace=namespace,
        )
    elif callable(view):
        pattern = Pattern(route, name=name, is_endpoint=True)
        return URLPattern(pattern, view, kwargs, name)
    else:
        raise TypeError('view must be a callable or a list/tuple in the case of include().')

pathではviewがリストかタプルだったらURLResolverが返り、そうじゃなくcallableだったらURLPatternクラスを返します。includeで指定した場合はタプルが返るのでURLResolverが返りますが、View(callable)を指定したら、__URLPattern__が返ります。

ここでは、

path('', views.IndexView.as_view(), name='index'),

とViewを指定しているので、

URLPattern('', view, kwargs, 'index')

といった風にURLPatternを宣言している事になります。
ここで渡しているviewはas_viewによって返された__dispatchメソッドを返すview__メソッドで、別記事でまとめました。

Djangoのas_viewについて

※また、URLPatternに渡しているpatternは、__RoutePattern__というクラスを指定していてURLの検索に使用します。

URLPatternクラス

urls.pyに記述したurlpatternsがループされると、このURLPatternクラスがループされresolveメソッドの中で、
__RoutePatternクラスのmatchメソッド__により、正規表現による該当URLの検索が行われ、それによりResolverMatchクラスが返されます。

django.urls.resolvers.py
class URLPattern:
    def resolve(self, path):
        # self.patternはRoutePatternクラス
        match = self.pattern.match(path)
        if match:
            new_path, args, kwargs = match
            kwargs.update(self.default_args)
            return ResolverMatch(self.callback, args, kwargs, self.pattern.name, route=str(self.pattern))

RoutePatternクラス

上記のURLResolverのresolveで呼ばれるmatchメソッドはこのクラスのメソッドです。
LocaleRegexDescriptorクラスから返される、re.compileの結果に対して、searchメソッドにより正規表現で該当のパスを検索します。

django.urls.resolvers.py
class RoutePattern:
    regex = LocaleRegexDescriptor('_route')
    
    def __init__(self, route, name=None, is_endpoint=False):
        self._route = route
        self._regex_dict = {}
        self._is_endpoint = is_endpoint
        self.name = name
        self.converters = _route_to_regex(str(route), is_endpoint)[1]

    def match(self, path):
        # ここでself.regexが呼ばれる事で、LocaleRegexDescriptorの__get__メソッドが呼ばれ、
        # re.compileが返りそれに対してsearchをかける。
        match = self.regex.search(path)
        if match:
            kwargs = match.groupdict()
            for key, value in kwargs.items():
                converter = self.converters[key]
                try:
                    kwargs[key] = converter.to_python(value)
                except ValueError:
                    return None
            return path[match.end():], (), kwargs
        return None

ResolverMatch

このクラスが返される事により、該当Viewの該当メソッド、該当引数らが返されます。

django.urls.resolvers.py
class ResolverMatch:
    def __getitem__(self, index):
        return (self.func, self.args, self.kwargs)[index]

※__getitem__の挙動が気になったのでチェックしてみました。

test.py
class Sample:
    def __init__(self, name, func=None):
        self.name = name
        setattr(self, 'func', func)

    def __getitem__(self, index):
        if hasattr(self, 'func'):
            return (self.name, self.func)[index]
        return (self.name)[index]

def foo():
    pass

s = Sample('abc', foo)
a, b = s
print(a, b)
output.sh
abc <function foo at ...>

と言う風にとれました^^
また、[]でs[0]みたいに指定しても呼ばれるみたいです。

②. トランザクションチェック

django.core.handlers.base.py
class BaseHandler:
    def make_view_atomic(self, view):
        non_atomic_requests = getattr(view, '_non_atomic_requests', set())
        for db in connections.all():
            if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
                if asyncio.iscoroutinefunction(view):
                    # asyncのviewではトランザクションは使用出来ない。
                    raise RuntimeError(
                        'You cannot use ATOMIC_REQUESTS with async views.'
                    )
                # トランザクション処理を行うViewとしてラップ
                view = transaction.atomic(using=db.alias)(view)
        return view    

ATOMIC_REQUESTSの設定がされていて、dbのaliasがviewの_non_atomic_requestsに含まれていなかったら、そのViewをトランザクション処理を行うViewとしてラップする。

③. 該当Viewの該当メソッドの呼び出し

この例では汎用Viewを定義しているので、as_viewによって渡したリクエストのメソッドの名前のメソッドが呼び出されます。
ここでは、リクエストがindexページへgetメソッドでやって来たと仮定すると、__IndexViewの親クラスのgetメソッド__が呼び出されます。

generics.ListViewはただ継承しているだけのクラスなので、BaseListViewのgetメソッドが呼び出されます。

django.views.generics.base.py
class BaseListView:
    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()
        ...
        context = self.get_context_data()
        return self.render_to_response(context)

render_to_responseメソッドの結果を返すみたいです。

django.views.generics.base.py
class TemplateResponseMixin:
    response_class = TemplateResponse

    def render_to_response(self, context, **response_kwargs):
        response_kwargs.setdefault('content_type', self.content_type)

        # 最終的にTemplateResponseクラスが返される。
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs
        )

getメソッドを追っていくと、最終的にTemplateResponseクラスが返されます。

④. TemplateResponse.render

そして、TemplateResponseクラスの親クラスには、renderメソッドが実装されているので、そのrenderメソッドが呼ばれる事でレスポンスをレンダリングします。

django.template.response.py
class TemplateResponse(SimpleTemplateResponse):
    rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']

    def __init__(self, request, template, context=None, content_type=None,
                 status=None, charset=None, using=None, headers=None):
        super().__init__(template, context, content_type, status, charset, using, headers=headers)
        self._request = request


class SimpleTemplateResponse(HttpResponse):

    def render(self):
        retval = self
        if not self._is_rendered:
            # self.rendered_contentにより、テンプレートがレンダリングされる。
            self.content = self.rendered_content

            # キャッシュミドルウェアとかで設定された処理を通す。
            for post_callback in self._post_render_callbacks:
                newretval = post_callback(retval)
                if newretval is not None:
                    retval = newretval
        return retval

レンダリングの部分

django.template.response.py
class SimpleTemplateResponse(HttpResponse):
    @property
    def rendered_content(self):
        template = self.resolve_template(self.template_names) # 1 テンプレートを取得
        context = self.resolve_context(self.context_data)     # 2 コンテキスト取得
        return template.render(context, self._request)        # 3 レンダリング
  1. self.resolve_template(テンプレートの取得)
  2. self.resolve_context(コンテキストの取得)
  3. template.render(テンプレートのレンダリング)

という流れで動いていきます。

1. self.resolve_template(テンプレートの取得)
django.template.response.py
class SimpleTemplateResponse(HttpResponse):
    
    def resolve_template(self, template):
        # ここでのtemplateは、IndexViewで指定したtemplate_nameがリストで渡されているためこっち。
        if isinstance(template, (list, tuple)):
            return select_template(template, using=self.using)

        elif isinstance(template, str):
            return get_template(template, using=self.using)
        else:
            return template
django.template.loader.py
def select_template(template_name_list, using=None):
    ...

    # テンプレートエンジンを取得
    engines = _engine_list(using)
    for template_name in template_name_list:
        for engine in engines:
            try:
                # テンプレートエンジンのget_templateメソッド
                return engine.get_template(template_name)
    ...

1-1. テンプレートエンジン取得の部分
django.template.loader.py
def _engine_list(using=None):
    # ここではusingは指定していないため、engine.all()が返る。
    return engines.all() if using is None else [engines[using]]
django.template.__init__.py
engines = EngineHanlder()

usingはNoneのため、EngineHandlerのallメソッドが返る。
このusingは、TemplateResponseクラスのインスタンス化時にself.template_engineとして渡しているもののようです。default=Noneなので、指定するには汎用Viewとかで指定しておく必要があります。
この記述の仕方的に、設定でテンプレートエンジンを複数記述しておき、使用するテンプレートエンジンをView毎に分けたりしたい時などに、それぞれのViewでテンプレートエンジンを指定しておけばいいものだと思います。

テンプレート設定

設定ファイルにはテンプレート設定はデフォルトだと大体こんな感じだと思います。複数指定するには、このTEMPLATEリストに辞書をつっこめばOKです。

settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        '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.utils.py
class EngineHandler:

    def __init__(self, templates=None):
        self._templates = templates
        self._engines = {}

    @cached_property
    def templates(self):
        # __iter__メソッドに最初に呼び出される。# 3
        # cached_propertyにより、次からはキャッシュから取得出来る。
 
        # 設定ファイルに記述したテンプレート設定を取得する。
        if self._templates is None:
            self._templates = settings.TEMPLATES
        templates = {}
        backend_names = []
        for tpl in self._templates:
            try:
                # 「.」で右から2回分割した時の右から2個めの文字列を取得。
                # Djangoのデフォルトだと、「django」を取得。
                default_name = tpl['BACKEND'].rsplit('.', 2)[-2]
            ...
            # 辞書にまとめる。
            tpl = {
                'NAME': default_name,
                'DIRS': [],
                'APP_DIRS': False,
                'OPTIONS': {},

                # **と付ける事で、テンプレート設定に基づいて辞書を上書きする。
                **tpl,
            }
            templates[tpl['NAME']] = tpl
            backend_names.append(tpl['NAME'])

        # 同じテンプレートエンジンが複数指定されていた場合エラー吐いたり..

        return templates
 
    def __getitem__(self, alias):
        # allメソッドのself[alias]という記述により呼び出される。 # 4
        try:
            # 既にself._enginesに設定されていたらそこから取得。
            return self._engines[alias]
        except KeyError:
            try:
                # テンプレート設定から作成した辞書からパラメータを取得。
                params = self.templates[alias]
            ...
            params = params.copy()
            backend = params.pop('BACKEND')
            engine_cls = import_string(backend)

            # テンプレートエンジンにパラメータを渡しインスタンス化。
            engine = engine_cls(params)
            # self._enginesにセットしておく。
            self._engines[alias] = engine
            return engine

    def __iter__(self):
        # self.templatesのgetterが呼ばれそれをイテレータとして返す。 # 2
        return iter(self.templates)

    def all(self):
        # ループするため__iter__が呼ばれる。 # 1
        return [self[alias] for alias in self]

動く順番としては、

①. allメソッドによりforを宣言
②. __iter__メソッドによりtemplatesをイテレータ化
③. templates設定取得(cached_propertyにより以降はキャッシュから取得)
④. __getitem__メソッドによりテンプレートエンジンをインスタンス化し取得
⑤. allメソッドによりテンプレートエンジンインスタンスのリストが返る

また、上記のコードで、ゲッターによりテンプレート設定から取得出来るテンプレート情報は以下の様になっています。

{
    'django':
        {
            'NAME': 'django',
            'DIRS': ['ここはpath'],
            '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'
                ]
             }, 
             'BACKEND': 'django.template.backends.django.DjangoTemplates'
         }
}

キーの__django__がaliasになっており、BACKENDはテンプレートエンジンをインスタンス化する際に必要なテンプレートエンジンへのpathを示しています。テンプレートエンジンインスタンス化の際にはNAME~BACKENDが引数として渡されます。

jinja2とかだったら、キーがjinja2になって、バリューにパラメータが入ります。

1-2. テンプレートエンジンのget_templateメソッド

ここではDjangoのデフォルトテンプレートエンジンを使用しているので、__django.template.backends.django.DjangoTemplates__のget_templateメソッドが呼ばれます。

django.template.backends.django.py
class DjangoTemplates(BaseEngine):

    def __init__(self, params):
        ...
        super().__init__(params)
        self.engine = Engine(self.dirs, self.app_dirs, **options)

    def get_template(self, template_name):
        try:
            return Template(self.engine.get_template(template_name), self)
django.template.backends.base.py
class BaseEngine:
    def __init__(self, params):
        # BaseEngineのinitでは、dirsとかapp_dirsとかを設定する。
        params = params.copy()
        self.name = params.pop('NAME')
        self.dirs = list(params.pop('DIRS'))
        self.app_dirs = params.pop('APP_DIRS')
        ...

DjangoTemplatesクラスでは、initでEngine等のパラメータを設定し、get_templateメソッドではテンプレートクラスを第1引数にはテンプレートクラスを、第2引数にはDjangoTemplatesクラスを渡しインスタンス化します。

このself.engine.get_templateメソッドはinitで指定した、Engineクラスのメソッドです。
このメソッドではファイル検索クラスLoaderによりテンプレートを検索する処理が走り、最終的にテンプレートクラスが返されます。
※返されるTemplateクラスはdjango.template.base.pyのものです。

django.template.engine.py
class Engine:
    def __init__(self, dirs=None, app_dirs=False, context_processors=None,
                 debug=False, loaders=None, string_if_invalid='',
                 file_charset='utf-8', libraries=None, builtins=None, autoescape=True):
        if dirs is None:
            dirs = []
        if context_processors is None:
            context_processors = []

        # これらのLoaderクラスにより、テンプレートファイルが検索されます。
        if loaders is None:
            loaders = ['django.template.loaders.filesystem.Loader']
            if app_dirs:
                loaders += ['django.template.loaders.app_directories.Loader']
            if not debug:
                loaders = [('django.template.loaders.cached.Loader', loaders)]
        ...
        self.dirs = dirs
        self.app_dirs = app_dirs
        ...

Engineクラスのinitで、テンプレートファイルを検索するためのLoaderクラスを指定しています。

django.template.engine.py
class Engine:

    def get_template(self, template_name):
        template, origin = self.find_template(template_name)
        if not hasattr(template, 'render'):
            template = Template(template, origin, template_name, engine=self)
        return template

    def find_template(self, name, dirs=None, skip=None):
        tried = []
        for loader in self.template_loaders:
            try:
                template = loader.get_template(name, skip=skip)
                return template, template.origin
            ...

そして、find_templateメソッド内で、Loaderクラスによるテンプレートファイルを検索する処理が走り、テンプレートクラスとoriginが返る。

Loaderクラスによるファイル検索処理らへんは省略します。

2. self.resolve_context(コンテキストの取得)

これは、ただcontextを取得するだけのメソッドが実装されているだけでした。

django.template.response.py
class SimpleTemplateResponse(HttpResponse):
    def resolve_context(self, context):
        return context
3. template.render(テンプレートのレンダリング)

ここでやっと、取得したTemplateクラスのrenderメソッドが呼ばれて、テンプレートのレンダリングが行われます。

django.template.backends.django.py
class Template:
    def __init__(self, template, backend):
        self.template = template
        self.backend = backend

    def render(self, context=None, request=None):
        context = make_context(context, request, autoescape=self.backend.engine.autoescape)
        try:
            return self.template.render(context)
        except TemplateDoesNotExist as exc:
            reraise(exc, self.backend)

まず、make_contextメソッドにより、contextを生成します。

django.template.context.py
def make_context(context, request=None, **kwargs):
    ...
    else:
        original_context = context
        context = RequestContext(request, **kwargs)
        if original_context:
            context.push(original_context)
    return context

contextを作ったらrenderメソッドにより、テンプレートのレンダリングを行います。

※このLoaderクラスによって取得したTemplateクラスは、上記のTemplateクラスとは別で、django.template.base.pyのTemplateクラスです。

django.template.base.py
class Template:
    ...

    def _render(self, context):
        return self.nodelist.render(context)

    def render(self, context):
        with context.render_context.push_state(self):
            if context.template is None:
                with context.bind_template(self):
                    context.template_name = self.name
                    return self._render(context)
            else:
                return self._render(context)

このレンダリング処理によって、テンプレートがレンダリングされ、最終的なテンプレートレスポンスがストリームに書き込まれ、クライアントに送信されるようですね。

ここらへんのレンダリング処理はまた奥が深そうなので、別でまとめたいと思います。

まとめ

【汎用ViewでViewを実装した場合】

ミドルウェアチェーンの流れの中で、process_request系の処理が終わったら

  1. URLResolverによって該当パス、View、引数等の取得
  2. as_viewによって該当のHTTPメソッドの名前のメソッドが呼ばれる
  3. renderが実装されていたら、テンプレートエンジンによりテンプレートのレンダリングが行われる(ミドルウェアによるprocess_responseの処理も行われる)
  4. レンダリングされたテンプレートレスポンスがストリームに書き込まれクライアントに送信される

という流れがおおまかに理解出来ました。

理解出来てないところもまだあるので、もっと細かい仕様の把握をしつつ、制作に生かせればと思います!

前回の記事はこちら。
Djangoのサーバーがリクエストを受け取ってからの流れ ①

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?