美文网首页Django 精研Django源码分析
django源码分析--02url解析

django源码分析--02url解析

作者: 极光火狐狸 | 来源:发表于2017-01-19 04:07 被阅读937次

    回顾上一章

    wsgi通过ServerHandler来执行django的应用程序,第一个落地对象是,django.contrib.staticfiles.handlers.StaticFilesHandler

    class StaticFilesHandler(WSGIHandler):
    
        def __init__(self, application):
            self.application = application
            super(StaticFilesHandler, self).__init__()
            
        def _should_handle(self, path):
            return path.startswith(self.base_url[2]) and not self.base_url[1]
            
        def get_response(self, request):
            from django.http import Http404
            if self._should_handle(request.path):
                try:
                    return self.serve(request)
                except Http404 as e:
                    if settings.DEBUG:
                        from django.views import debug
                        return debug.technical_404_response(request, e)
            return super(StaticFilesHandler, self).get_response(request)
            
        def __call__(self, environ, start_response):
            if not self._should_handle(get_path_info(environ)):
                return self.application(environ, start_response)
    
            return super(StaticFilesHandler, self).__call__(environ, start_response)
    

    django.contrib.staticfiles.handlers.StaticFilesHandler的作用是当请求出现时,先检查url是不是一个静态文件请求,如果是的话则进入静态文件(图片、css样式文件、js脚本文件等等)处理的view,如果不是的话则将该请求提交给Django的handler来进行处理(解析url、执行对应的view代码块、渲染和返回template)。

    补充说明
    __call__方法中比较有意思的是根据条件进行不同的返回。

    • self.application(environ, start_response)
      执行的是django.core.handlers.wsgi.WSGIHandler.__call__对象,对象的继承集合中不包含django.contrib.staticfiles.StaticFilesHandler,也就是说后续如果调用了self.get_response,那么它执行的是django.core.handlers.base.BaseHandler.get_response方法。
    • super(StaticFilesHandler, self).call(environ, start_response)
      虽然执行的也是django.core.handlers.wsgi.WSGIHandler.__call__对象,但是这个对象的继承集合里面包含django.contrib.staticfiles.StaticFilesHandler,因此后续如果调用了self.get_response,那么它执行的是django.contrib.staticfiles.StaticFilesHandler.get_response方法。

     
     

    URL解析

    django.core.handlers.wsgi.py
    class WSGIHandler(base.BaseHandler):
        request_class = WSGIRequest
    
        def __init__(self, *args, **kwargs):
            super(WSGIHandler, self).__init__(*args, **kwargs)
            self.load_middleware()
    
        def __call__(self, environ, start_response):
            set_script_prefix(get_script_name(environ))
            signals.request_started.send(sender=self.__class__, environ=environ)
            try:
                request = self.request_class(environ)
            except UnicodeDecodeError:
                logger.warning(
                    'Bad Request (UnicodeDecodeError)',
                    exc_info=sys.exc_info(),
                    extra={
                        'status_code': 400,
                    }
                )
                response = http.HttpResponseBadRequest()
            else:
                response = self.get_response(request)
    
            response._handler_class = self.__class__
    
            status = '%d %s' % (response.status_code, response.reason_phrase)
            response_headers = [(str(k), str(v)) for k, v in response.items()]
            for c in response.cookies.values():
                response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
            start_response(force_str(status), response_headers)
            if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
                response = environ['wsgi.file_wrapper'](response.file_to_stream)
            return response
    

    request = self.request_class(environ)负责初始化 WSGIRequest对象(这个对象有点想wsgi接口中的WSGIRequestHandler对象做的工作,针对environ进行预处理),在初始化过程中主要是在原本environ中加工处理一些django程序能读得懂的参数(例如header、PATH_INFO、REQUEST_METHOD、charset等等)。
    response = self.get_response(request)正如上面的补充说明环节说明一样,针对不同请求执行不同的方法,我主要是跟常规请求而不是静态文件请求,因此这个self.get_response实际上指的是django.core.handlers.base.BaseHandler.get_response

    django.core.handlers.base.py
    class BaseHandler(object):
    
        def __init__(self):
            ...
            self._middleware_chain = None
    
        def load_middleware(self):
            ...
    
            if settings.MIDDLEWARE is None:
                ...
            else:
                handler = convert_exception_to_response(self._get_response)
                ...
    
            self._middleware_chain = handler
    
            
        def get_response(self, request):
            ...
            response = self._middleware_chain(request)  
            ...
            return response    
            
        def _get_response(self, request):
            response = None
    
            if hasattr(request, 'urlconf'):
                urlconf = request.urlconf
                set_urlconf(urlconf)
                resolver = get_resolver(urlconf)
            else:
                resolver = get_resolver()           # 初始化RegexURLResolver对象
    
            resolver_match = resolver.resolve(request.path_info)
            callback, callback_args, callback_kwargs = resolver_match
            request.resolver_match = resolver_match
            ...
            if response is None:
                wrapped_callback = self.make_view_atomic(callback)
    
                try:
                    response = wrapped_callback(request, *callback_args, **callback_kwargs)
                except Exception as e:
                    response = self.process_exception_by_middleware(e, request)
            ...
            return response        
    

    response = self._middleware_chain(request),当执行完这行代码时,response变量就是一个html了,也就是说,所有的工作都隐藏在这行代码中。然而self._middleware_chain变量初始化的时候是None,然后在get_response方法中却可以被调用,这表明它是至少是一个方法或函数,也就是说,在调用get_response方法之前,这个self._middleware_chain变量已经被赋值过了,所以我要去找哪里有针对这个self._middleware_chain处理的地方。

    • django.core.handlers.base.BaseHandler.load_middleware方法中将convert_exception_to_response(self._get_response)赋值给了self._middleware_chain
    • convert_exception_to_response是一个装饰器,当self._get_response执行过程中遇到错误时,则try住这个错误并根据当前错误代码返回一个对应的错误信息页面。

    django.core.handlers.base.BaseHandler._get_response 负责解析URL和传递参数给view。

    • resolver = get_resolver() 初始化RegexURLResolver对象。
    • resolver_match = resolver.resolve(request.path_info) 通过path_info解析url所绑定的对象,并将解析结果对象赋值给resolver_match变量。
    • callback, callback_args, callback_kwargs = resolver_match 其中callback是具体的view, callback_args是执行这个view需要提供的列表参数(类似与tornado中的self.path_args),callback_kwargs是执行这个view需要提供的字典参数。
    • wrapped_callback = self.make_view_atomic(callback)是针对数据库事务支持所封装的一个对象。
    • response = wrapped_callback(request, *callback_args, **callback_kwargs)是执行这个具体的view(request, *callback_args, **callback_kwargs)。

    小结
    流程性的记录了request从初始化到url解析、再到response执行和返回过程。

     
     
     

    深入理解URL解析

    上一节有提及到resolver = get_resolver()resolver_match = resolver.resolve(request.path_info)这两行代码,它就是理解URL解析的入口,另外一个理解URL解析的入口在项目文件的urls.py文件中(也就是我当前HelloWorld项目的HelloWorld.urls.py)。

    HelloWorld.urls.py
    from django.conf.urls import url    # 这里是重点
    from django.contrib import admin
    
    urlpatterns = [
        url(r'^admin/', admin.site.urls),
    ]
    
    

    url(r'^admin/', admin.site.urls)这里采用了url()函数对正则表达式的url和视图进行包裹,反过来看就是将正则表达式url和视图当作参数传递给url()函数。
    备注: admin.site.urls是一个tuple,里面包含一个url集合(另外一组urlpatterns)。

     

    django.conf.urls.__init__.py
    from django.urls import (
        LocaleRegexURLResolver, RegexURLPattern, RegexURLResolver,
    )
    
    def url(regex, view, kwargs=None, name=None):
        if isinstance(view, (list, tuple)):
            # For include(...) processing.
            urlconf_module, app_name, namespace = view
            return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
        elif callable(view):
            return RegexURLPattern(regex, view, kwargs, name)
        else:
            raise TypeError('view must be a callable or a list/tuple in the case of include().')
    

    通过查看url()函数的定义,regex参数可以看作是r'^admin/', view参数可以看作是admin.site.urls。接下来是根据view参数的对象类型来调用不同的对象进行解析,因此这里临时插入一段代码来看看admin.site.urls。

    django.contrib.admin.sites.py
    class AdminSite(object):
    
        def __init__(self, name='admin'):
           self.name = name
            
        @property
        def urls(self):
            return self.get_urls(), 'admin', self.name
    
        def get_urls(self):
            from django.conf.urls import url, include
    
            def wrap(view, cacheable=False):
                def wrapper(*args, **kwargs):
                    return self.admin_view(view, cacheable)(*args, **kwargs)
                wrapper.admin_site = self
                return update_wrapper(wrapper, view)
    
            urlpatterns = [
                url(r'^$', wrap(self.index), name='index'),
                url(r'^login/$', self.login, name='login'),
                url(r'^logout/$', wrap(self.logout), name='logout'),
                url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'),
                url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), name='password_change_done'),
                url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
                url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut), name='view_on_site'),
            ]
    
            valid_app_labels = []
            for model, model_admin in self._registry.items():
                urlpatterns += [
                    url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
                ]
                if model._meta.app_label not in valid_app_labels:
                    valid_app_labels.append(model._meta.app_label)
    
            if valid_app_labels:
                regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$'
                urlpatterns += [
                    url(regex, wrap(self.app_index), name='app_list'),
                ]
            return urlpatterns
    
    site = AdminSite()
    

    透过这个代码片段可以清晰的看到admin.site.urls 实际上是一个元祖(tuple)对象,因此会匹配到if isinstance(view, (list, tuple))条件,并执行该条件下的代码块(返回RegexURLResolver初始化后的对象)。

    urlconf_module, app_name, namespace = view这里将view拆分成了三个变量(对象)。

    • urlconf_module = self.get_urls() = 整个admin的所有定义的urls.
    • app_name = 'admin' = 常量变量'admin'字符串
    • namespace = self.name = 'admin' = 常量变量'admin'字符串,由于site = AdminSite()初始化过程中并没有提供任何参数,因此采用了默认的def __init__(self, name='admin')

    另外一个情况是当view是单个函数时,则会匹配到elif callable(view)并执行该条件下的代码块(返回RegexURLPattern初始化后的对象)。

    小结
    上面三个代码片段主要是为了接下来的django.urls.resolvers.RegexURLResolver对象的原理理解做一个铺垫,因此下面我会言归正传,接着django.core.handlers.base.BaseHandler._get_response往下走。

     

    django.urls.resolvers.py
    class ResolverMatch(object):
        def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None):
            self.func = func
            self.args = args
            self.kwargs = kwargs
            self.url_name = url_name
    
            self.app_names = [x for x in app_names if x] if app_names else []
            self.app_name = ':'.join(self.app_names)
            self.namespaces = [x for x in namespaces if x] if namespaces else []
            self.namespace = ':'.join(self.namespaces)
    
            if not hasattr(func, '__name__'):
                self._func_path = '.'.join([func.__class__.__module__, func.__class__.__name__])
            else:
                self._func_path = '.'.join([func.__module__, func.__name__])
    
            view_path = url_name or self._func_path
            self.view_name = ':'.join(self.namespaces + [view_path])
    
        def __getitem__(self, index):
            return (self.func, self.args, self.kwargs)[index]
    
        def __repr__(self):
            return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s)" % (
                self._func_path, self.args, self.kwargs, self.url_name,
                self.app_names, self.namespaces,
            )
    
    
    @lru_cache.lru_cache(maxsize=None)
    def get_resolver(urlconf=None):
        if urlconf is None:
            from django.conf import settings
            urlconf = settings.ROOT_URLCONF
        return RegexURLResolver(r'^/', urlconf)
    
    
    class LocaleRegexProvider(object):
    
        def __init__(self, regex):
            self._regex = regex
            self._regex_dict = {}
    
        @property
        def regex(self):
            language_code = get_language()
            if language_code not in self._regex_dict:
                regex = self._regex if isinstance(self._regex, six.string_types) else force_text(self._regex)
                try:
                    compiled_regex = re.compile(regex, re.UNICODE)              # initially regex -> '^/'
                except re.error as e:
                    raise ImproperlyConfigured(
                        '"%s" is not a valid regular expression: %s' %
                        (regex, six.text_type(e))
                    )
                self._regex_dict[language_code] = compiled_regex
            return self._regex_dict[language_code]
    
    
    class RegexURLResolver(LocaleRegexProvider):
        def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
            LocaleRegexProvider.__init__(self, regex)
            self.urlconf_name = urlconf_name
            self.callback = None
            self.default_kwargs = default_kwargs or {}
            self.namespace = namespace
            self.app_name = app_name
            self._reverse_dict = {}
            self._namespace_dict = {}
            self._app_dict = {}
            self._callback_strs = set()
            self._populated = False
            self._local = threading.local()
            
        def resolve(self, path):
            path = force_text(path)  # path may be a reverse_lazy object
            tried = []
            match = self.regex.search(path)
            if match:
                new_path = path[match.end():]
                for pattern in self.url_patterns:
                    try:
                        sub_match = pattern.resolve(new_path)
                    except Resolver404 as e:
                        sub_tried = e.args[0].get('tried')
                        if sub_tried is not None:
                            tried.extend([pattern] + t for t in sub_tried)
                        else:
                            tried.append([pattern])
                    else:
                        if sub_match:
                            sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
                            sub_match_dict.update(sub_match.kwargs)
                            sub_match_args = sub_match.args
                            if not sub_match_dict:
                                sub_match_args = match.groups() + sub_match.args
    
                            return ResolverMatch(
                                sub_match.func,
                                sub_match_args,
                                sub_match_dict,
                                sub_match.url_name,
                                [self.app_name] + sub_match.app_names,
                                [self.namespace] + sub_match.namespaces,
                            )
                        tried.append([pattern])
                raise Resolver404({'tried': tried, 'path': new_path})
            raise Resolver404({'path': path})
    
        @cached_property
        def urlconf_module(self):
            if isinstance(self.urlconf_name, six.string_types):
                return import_module(self.urlconf_name)
            else:
                return self.urlconf_name
    
        @cached_property
        def url_patterns(self):
            patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
            try:
                iter(patterns)
            except TypeError:
                msg = (
                    "The included URLconf '{name}' does not appear to have any "
                    "patterns in it. If you see valid patterns in the file then "
                    "the issue is probably caused by a circular import."
                )
                raise ImproperlyConfigured(msg.format(name=self.urlconf_name))
            return patterns        
    

    django.core.handlers.base.BaseHandler._get_response方法中通过resolver = get_resolver()来初始化RegexURLResolver对象,它调用了当前代码片段中的get_resolver,返回值是RegexURLResolver(r'^/', urlconf)相当于RegexURLResolver(r'^/', 'HelloWorld.urls')。

    • RegexURLResolver.__init__
      初始化过程中,会将r'^/'赋值给self._regex(以super的形式在父类中赋值),将'HelloWorld.urls'赋值给self.urlconf_name

    • RegexURLResolver.urlconf_module
      利用python Lib库中的高级特性import_module,先创建一个空的对象,然后将'HelloWorld.urls'这个字符串当作一个模块文件来执行,并将所有出现在这个模块文件中的变量以attribute的形式set到这个对象中,最后把这个对象返回给调用者。
      通过上面提供的HelloWorld.urls.py文件代码块的内容可以看的出来, urlpatterns 是一个列表对象(变量),还有url和admin这两个对象(变量),因此这个import_module创建的变量就包含了urlpatterns、url、admin这三个attribute。

    • RegexURLResolver.url_patterns
      这个方法调用了urlconf_module方法,然后提取出urlpatterns的值,最后把这个值返回给调用者。

    django.core.handlers.base.BaseHandler._get_response 方法中通过resolver_match = resolver.resolve(request.path_info)来返回url解析结果,该url解析结果包含了对应的view、该view执行需要用到的args和该view执行需要用到的kwargs。resolver.resolve(request.path_info)相当于是RegexURLResolver(r'^/', 'HelloWorld.urls').resolve(request.path_info)

    • RegexURLResolver.resolve 这个函数非常烧脑,我尽量列详细记录,避免以后忘了可以再过来看快速记起来。

      • path = force_text(path) 将路径强制转换成字符串.

      • tried = [] 用于记录已解析对象(路径).

      • match = self.regex.search(path)
        通过继承对象LocaleRegexProvider.regex方法来匹配路径,由于整个当前对象RegexURLResolver初始化时,传递的参数是'^/',因此不论请求过来的URL是什么,都会匹配成功,因此if match是肯定通过的。它还有另外一层作用(职责),就是专门负责匹配到'模块',例如: 请求的url是</admin/auth/user/>,那么它将会匹配到'^/'。

      • new_path = path[match.end():]
        这行代码的用意是将上级匹配到的'模块'给移除掉。

      • for pattern in self.url_patterns
        这里self.url_patterns返回的是一个包裹了HelloWorld.urls的 RegexURLResolver 对象集合(由于我没有自定义填写任何新的url,因此它只有[url(r'^admin/', admin.site.urls)],经过url这个函数执行过后,它实际上返回的是这么一个东西:[<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>])。
        通过for循环将这个[<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>]进行分解,其实当前状态下列表只有一个元素,因此这个for循环只会执行一次,但是它的作用不止于此,还需要耐心往下看。

      • sub_match = pattern.resolve(new_path)
        在这里,这个pattern就是<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>这个对象,pattern.resolve相当于RegexURLResolver.resolve,看起来像是自己把自己再次调用了一遍死循环的节奏,但是由于pattern是外部重新实例化的一个对象,因此这里并不是递归,也不是死循环。

        admin.site.urls == <RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>,再次执行上面所提到的代码,但是当再次遇到for pattern in self.url_patterns返回的对象就不一样了,这次它返回的是下面这些对象(为什么会是这些东西,请看前面有列出来的get_urls()的源码):

        [
        <RegexURLPattern index ^$>,
        <RegexURLPattern login ^login/$>,
        <RegexURLPattern logout ^logout/$>,
        <RegexURLPattern password_change ^password_change/$>,
        <RegexURLPattern password_change_done ^password_change/done/$>,
        <RegexURLPattern jsi18n ^jsi18n/$>,
        <RegexURLPattern view_on_site ^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$>,
        <RegexURLResolver <RegexURLPattern list> (None:None) ^auth/group/>,
        <RegexURLResolver <RegexURLPattern list> (None:None) ^auth/user/>,
        <RegexURLPattern app_list ^(?P<app_label>auth)/$>
        ]

        这次pattern.resolve的对象就不一样了,当pattern对象是RegexURLPattern时,执行的其实RegexURLPattern.resolve,它的作用是利用每个RegexURLPattern中的正则表达式来匹配被裁(new_path = path[match.end():])剪过的url; 当pattern对象还是RegexURLResolver时,再去找到该对象下的所有urlpatterns集合。

      • 剩下的就不是很难,所以就不列出来了。

    相关文章

      网友评论

        本文标题:django源码分析--02url解析

        本文链接:https://www.haomeiwen.com/subject/qvabbttx.html