Django の urlresolvers.py を読む

2008/04/22 10:38am

一身上の都合により DjangoURL dispatcher について調べている。ドキュメントでだいたいの動作は把握したので、今回は urls.py に定義した URL のマッピングが実際にはどのように解決されているのか、ソースコードを読むことで理解を深めたい、と思う。

なお、参照したソースコードは SVN の Revision 7438 である。公開リリースの 0.96.1 とは内容が大きく異なる可能性があること、 Trac での議論やドキュメントを読むかぎり、このへんの実装については今後大きく変更されるかもしれないことを、あらかじめお断りしておく。

urlresolvers.py

まずは、調べるソースコードを特定しよう。URL の逆マッピングをしてくれる reverse() 関数が用意されていることは分かっているので、これを手がかりにソースコードを探すのがよさそうだ。

試しに def reverse( を検索してみると、簡単に見つかった。django/core/urlresolvers.py で定義されている。ソースコードのコメントにも、

This module converts requested URLs to callback view functions.

と書かれているので、これが探していたソースコード(モジュール)だろう。

reverse() と resolve()

urlresolvers.py では reverse() だけでなく、resolve() も定義されている。

def resolve(path, urlconf=None):
    return get_resolver(urlconf).resolve(path)

def reverse(viewname, urlconf=None, args=None, kwargs=None):
    args = args or []
    kwargs = kwargs or {}
    return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs))

関数名や引数から reverse() が URL の逆マッピング(名前、引数から URL を探索)を行うのにたいし、resolve() が URL のマッピング(URL から探索)を担当していることは容易に想像がつく。

では、resolve() が返すものはなんだろうか? これはソースコードのコメントに書いてある。

This module converts requested URLs to callback view functions.

RegexURLResolver is the main class here. Its resolve() method takes a URL (as a string) and returns a tuple in this format:

(view_function, function_args, function_kwargs)

アクセスされた URL パス文字列を渡すと、

がタプルで返ってくるらしい。

RegexURLResolver

reverse()resolve() も、実際の処理は get_resolver() で返ってくるオブジェクトに丸投げしている。そして、先のコメントが示唆するように、get_resolver() で返されるオブジェクトは RegexURLResolver のインスタンスになっている。

def get_resolver(urlconf):
...
    return RegexURLResolver(r'^/', urlconf)

RegexURLResolverurls.py に対応したクラスであり、そこで定義された urlpatternshandler404 などを参照している。

class RegexURLResolver(object):
...
    def _get_urlconf_module(self):
        try:
            return self._urlconf_module
        except AttributeError:
            try:
                self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
...
    def _get_url_patterns(self):
        return self.urlconf_module.urlpatterns
...
    def _resolve_special(self, view_type):
        callback = getattr(self.urlconf_module, 'handler%s' % view_type)
...
    def resolve404(self):
        return self._resolve_special('404')

    def resolve500(self):
        return self._resolve_special('500')

なお、urlpatterns の中身は何かといえば、同じファイルで定義されている RegexURLPattern インスタンスのリストだ(ただし、別の urls.pyinclude() している場合は RegexURLPattern ではなく RegexURLResolver インスタンスがリストに挿入されて入れ子構造になる)。このへんは patterns() や url() が定義されている django/conf/defaults.py を読むと分かる。

resolve() メソッドの実装を見れば、URL パス文字列の対応を urlpatterns から探索する様子が分かるだろう。

def resolve(self, path):
    tried = []
    match = self.regex.search(path)
    if match:
        new_path = path[match.end():]
        for pattern in self.urlconf_module.urlpatterns:
            try:
                sub_match = pattern.resolve(new_path)
            except Resolver404, e:
                tried.extend([(pattern.regex.pattern + '   ' + t) for t in e.args[0]['tried']])
            else:
                if sub_match:
                    sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
                    sub_match_dict.update(self.default_kwargs)
                    for k, v in sub_match[2].iteritems():
                        sub_match_dict[smart_str(k)] = v
                    return sub_match[0], sub_match[1], sub_match_dict
                tried.append(pattern.regex.pattern)
        raise Resolver404, {'tried': tried, 'path': new_path}

resolve() メソッドは RegexURLPattern にも RegexURLResolver にも実装されていることに注意。

MatchChecker

urlresolver.py にはこの他に MatchChecker というクラスが定義されており、これは reverse() の過程で正規表現をパス文字列に変換するために使われているようだ。

def reverse_helper(regex, *args, **kwargs):
...
    # TODO: Handle nested parenthesis in the following regex.
    result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
    return result.replace('^', '').replace('$', '')

余談だが、ここでは ^$ のメタ文字しか削除していないため、他のメタ文字はそのまま残ってしまう(もっとも、通常のマッピングをする範囲で困ることはないだろうけど)。