Flask源码解读(3) -- route

来源:互联网 发布:大数据和数据挖掘 编辑:程序博客网 时间:2024/06/01 23:25

 Flask源码解读(3) -- route

在我们编写app程序代码时, 会编写下面的程序

@app.route('/')     def index():         return 'Hello World'
表示通过app为url '/' 注册视图函数 index, 当客户端访问'/'时, 调用index, 生成response. 现在我们分析一下注册route的过程


Flask的route方法定义如下

class Flask(_PackageBoundObject): 
    def route(self, rule, **options):
def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator

app.route实际是带有参数的装饰器, 将rule(就是url), endpoint(和rule一一对应), f(我们定义的视图函数)传入到app.add_url_rule方法中

class Flask():    def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options):        if endpoint is None:            endpoint = _endpoint_from_view_func(view_func)        options['endpoint'] = endpoint        methods = options.pop('methods', None)        # if the methods are not given and the view_func object knows its        # methods we can use that instead.  If neither exists, we go with        # a tuple of only ``GET`` as default.        if methods is None:            methods = getattr(view_func, 'methods', None) or ('GET',)        if isinstance(methods, string_types):            raise TypeError('Allowed methods have to be iterables of strings, '                            'for example: @app.route(..., methods=["POST"])')        methods = set(item.upper() for item in methods)        # Methods that should always be added        required_methods = set(getattr(view_func, 'required_methods', ()))        # starting with Flask 0.8 the view_func object can disable and        # force-enable the automatic options handling.        if provide_automatic_options is None:            provide_automatic_options = getattr(view_func,                'provide_automatic_options', None)        if provide_automatic_options is None:            if 'OPTIONS' not in methods:                provide_automatic_options = True                required_methods.add('OPTIONS')            else:                provide_automatic_options = False        # Add the required methods now.        methods |= required_methods        rule = self.url_rule_class(rule, methods=methods, **options)        rule.provide_automatic_options = provide_automatic_options        self.url_map.add(rule)        if view_func is not None:            old_func = self.view_functions.get(endpoint)            if old_func is not None and old_func != view_func:                raise AssertionError('View function mapping is overwriting an '                                     'existing endpoint function: %s' % endpoint)            self.view_functions[endpoint] = view_func
在add_url_rule方法中, 在传入的endpoint为None的情况下, endpoint = _endpoint_from_view_func(view_func)会根据视图函数构造endpoint, 这里其实就是返回了函数名.

就是说每当我们定义了一个rule和视图函数, 也即有个和视图函数同名的endpoint和rule对应. 

随后在默认的情况下, 为rule添加GET访问方法.

随后rule = self.url_rule_class(rule, methods=methods, **options), app.url_rule_class是werkzeug.routing模块中的Rule类. 

随后将rule添加到app.url_map中.


Rule类的部分定义如下:

class Rule(RuleFactory):    def __init__(self, string, defaults=None, subdomain=None, methods=None,                 build_only=False, endpoint=None, strict_slashes=None,                 redirect_to=None, alias=False, host=None):        if not string.startswith('/'):            raise ValueError('urls must start with a leading slash')        self.rule = string        self.is_leaf = not string.endswith('/')        self.map = None        self.strict_slashes = strict_slashes        self.subdomain = subdomain        self.host = host        self.defaults = defaults        self.build_only = build_only        self.alias = alias        if methods is None:            self.methods = None        else:            if isinstance(methods, str):                raise TypeError('param `methods` should be `Iterable[str]`, not `str`')            self.methods = set([x.upper() for x in methods])            if 'HEAD' not in self.methods and 'GET' in self.methods:                self.methods.add('HEAD')        self.endpoint = endpoint        self.redirect_to = redirect_to        if defaults:            self.arguments = set(map(str, defaults))        else:            self.arguments = set()        self._trace = self._converters = self._regex = self._weights = None        def bind(self, map, rebind=False):        """Bind the url to a map and create a regular expression based on        the information from the rule itself and the defaults from the map.        :internal:        """        if self.map is not None and not rebind:            raise RuntimeError('url rule %r already bound to map %r' %                               (self, self.map))        self.map = map        if self.strict_slashes is None:            self.strict_slashes = map.strict_slashes        if self.subdomain is None:            self.subdomain = map.default_subdomain        self.compile()        def compile(self):        """Compiles the regular expression and stores it."""        assert self.map is not None, 'rule not bound'        if self.map.host_matching:            domain_rule = self.host or ''        else:            domain_rule = self.subdomain or ''        self._trace = []        self._converters = {}        self._weights = []        regex_parts = []        def _build_regex(rule):            for converter, arguments, variable in parse_rule(rule):                if converter is None:                    regex_parts.append(re.escape(variable))                    self._trace.append((False, variable))                    for part in variable.split('/'):                        if part:                            self._weights.append((0, -len(part)))                else:                    if arguments:                        c_args, c_kwargs = parse_converter_args(arguments)                    else:                        c_args = ()                        c_kwargs = {}                    convobj = self.get_converter(                        variable, converter, c_args, c_kwargs)                    regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))                    self._converters[variable] = convobj                    self._trace.append((True, variable))                    self._weights.append((1, convobj.weight))                    self.arguments.add(str(variable))        _build_regex(domain_rule)        regex_parts.append('\\|')        self._trace.append((False, '|'))        _build_regex(self.is_leaf and self.rule or self.rule.rstrip('/'))        if not self.is_leaf:            self._trace.append((False, '/'))        if self.build_only:            return        regex = r'^%s%s$' % (            u''.join(regex_parts),            (not self.is_leaf or not self.strict_slashes) and            '(?<!/)(?P<__suffix__>/?)' or ''        )        self._regex = re.compile(regex, re.UNICODE)    def match(self, path, method=None):        """Check if the rule matches a given path. Path is a string in the        form ``"subdomain|/path"`` and is assembled by the map.  If        the map is doing host matching the subdomain part will be the host        instead.        If the rule matches a dict with the converted values is returned,        otherwise the return value is `None`.        :internal:        """        if not self.build_only:            m = self._regex.search(path)            if m is not None:                groups = m.groupdict()                # we have a folder like part of the url without a trailing                # slash and strict slashes enabled. raise an exception that                # tells the map to redirect to the same url but with a                # trailing slash                if self.strict_slashes and not self.is_leaf and \                        not groups.pop('__suffix__') and \                        (method is None or self.methods is None or                         method in self.methods):                    raise RequestSlash()                # if we are not in strict slashes mode we have to remove                # a __suffix__                elif not self.strict_slashes:                    del groups['__suffix__']                result = {}                for name, value in iteritems(groups):                    try:                        value = self._converters[name].to_python(value)                    except ValidationError:                        return                    result[str(name)] = value                if self.defaults:                    result.update(self.defaults)                if self.alias and self.map.redirect_defaults:                    raise RequestAliasRedirect(result)                return result   
app中的rule添加到map中时, 会调用rule.compile方法. 我们举个例子说明rule.compile的作用:

@app.route('/somepage'):

def view_fun():

pass

当构造Rule实例时, rule.compile会返回匹配'/somepage'的正则表达式, 用于后续的匹配. 由于'/somepage'是一个静态url, 所以返回的正则表达式就是他本身.

但如果url是动态的, 如'/item/<int:id>', id表示item的编码, 不用的八位编码对应不同的item. 这个url可以匹配'/item/123456', 也可以匹配'/item/654321'


看到rule.compile中的_build_regex函数

语句for converter, arguments, variable in parse_rule(rule) 通过parse_rule函数解析rule

parse_rule定义如下:

def parse_rule(rule):    """Parse a rule and return it as generator. Each iteration yields tuples    in the form ``(converter, arguments, variable)``. If the converter is    `None` it's a static url part, otherwise it's a dynamic one.    :internal:    """    pos = 0    end = len(rule)    do_match = _rule_re.match    used_names = set()    while pos < end:        m = do_match(rule, pos)        if m is None:            break        data = m.groupdict()        if data['static']:            yield None, None, data['static']        variable = data['variable']        converter = data['converter'] or 'default'        if variable in used_names:            raise ValueError('variable name %r used twice.' % variable)        used_names.add(variable)        yield converter, data['args'] or None, variable        pos = m.end()    if pos < end:        remaining = rule[pos:]        if '>' in remaining or '<' in remaining:            raise ValueError('malformed url rule: %r' % rule)        yield None, None, remaining

通过一个循环, 在url内不断执行正则表达式的match操作, _rule_re就表示该正则表达式

_rule_re = re.compile(r'''    (?P<static>[^<]*)                           # static rule data    <    (?:        (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*)   # converter name        (?:\((?P<args>.*?)\))?                  # converter arguments        \:                                      # variable delimiter    )?    (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*)        # variable name    >''', re.VERBOSE)
上面的正则表达式中, 开始是一个命名分组static, 表示url中的静态部分, 匹配不是'<'的任意字符. 紧接着由'<>'包含着url中的动态部分.  看到前面我们的例子中, 动态url的动态部分确实是由<>包含. '<>'内包含三个命名分组, 前两个为converter, args. 表示动态部分的转换器(对应例子中的int, 作用是匹配后面的url时, 将字符串形式的数字转换成python的int型), 和转换参数(例如可以指定int类型的宽度), 这两个命名分组可以没有. 第三个命名分组是variable就是动态部分的变量(例子中的id). converter args 和variable之间用':'分割

回到parse_rule函数中, 如果匹配的是url中的静态部分则yield None, None, data['static']. 这里表明对于静态部分, 生成的正则表达式就是他自己

如果匹配的是url中的动态部分, 则yield converter, data['args'] or None, variable. 在_build_regex函数中, regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))语句表明

将生成converter转换成的命名分组. 下面是一个例子:

>>> from werkzeug.routing import Rule, Map
>>> m = Map()
>>> r = Rule('/item/<int(6):id>')
>>> r.bind(m)
>>> r._regex
<_sre.SRE_Pattern object at 0x7fa10a377210>
>>> r._regex.pattern
u'^\\|\\/item\\/(?P<id>\\d+)$'

r._regex.pattern 中前面部分的'\\|'先不用考虑, 后半部分就是根据我们定义的url生成的用于匹配的正则表达式.


另外Flask中还有一种通过blueprint定义路由的方式:

class Blueprint(_PackageBoundObject):       def __init__(self, name, import_name, static_folder=None,                 static_url_path=None, template_folder=None,                 url_prefix=None, subdomain=None, url_defaults=None,                 root_path=None):        _PackageBoundObject.__init__(self, import_name, template_folder,                                     root_path=root_path)        self.name = name        self.url_prefix = url_prefix        self.subdomain = subdomain        self.static_folder = static_folder        self.static_url_path = static_url_path        self.deferred_functions = []        if url_defaults is None:            url_defaults = {}        self.url_values_defaults = url_defaults    def record(self, func):               if self._got_registered_once and self.warn_on_modifications:            from warnings import warn            warn(Warning('The blueprint was already registered once '                         'but is getting modified now.  These changes '                         'will not show up.'))        self.deferred_functions.append(func)    def record_once(self, func):               def wrapper(state):            if state.first_registration:                func(state)        return self.record(update_wrapper(wrapper, func))    def make_setup_state(self, app, options, first_registration=False):                return BlueprintSetupState(self, app, options, first_registration)    def register(self, app, options, first_registration=False):                self._got_registered_once = True        state = self.make_setup_state(app, options, first_registration)        if self.has_static_folder:            state.add_url_rule(                self.static_url_path + '/<path:filename>',                view_func=self.send_static_file, endpoint='static'            )        for deferred in self.deferred_functions:            deferred(state)    def route(self, rule, **options):               def decorator(f):            endpoint = options.pop("endpoint", f.__name__)            self.add_url_rule(rule, endpoint, f, **options)            return f        return decorator    def add_url_rule(self, rule, endpoint=None, view_func=None, **options):                if endpoint:            assert '.' not in endpoint, "Blueprint endpoints should not contain dots"        if view_func:            assert '.' not in view_func.__name__, "Blueprint view function name should not contain dots"        self.record(lambda s:            s.add_url_rule(rule, endpoint, view_func, **options))    def endpoint(self, endpoint):                def decorator(f):            def register_endpoint(state):                state.app.view_functions[endpoint] = f            self.record_once(register_endpoint)            return f        return decorator    
当我们创建Blueprint对象并注册到app中后,  便可以通过blueprint注册路由. 注册过程和通过app注册类似, 稍有不同:

add_url_rule方法最后注册了一个匿名函数, 这个匿名函数会在blueprint被注册到app时传入BlueprintSetupState类的对象s并执行. 执行的结果就是调用s.add_url_rule方法

class BlueprintSetupState(object):       def __init__(self, blueprint, app, options, first_registration):        self.app = app        self.blueprint = blueprint        self.options = options        self.first_registration = first_registration        subdomain = self.options.get('subdomain')        if subdomain is None:            subdomain = self.blueprint.subdomain        self.subdomain = subdomain        url_prefix = self.options.get('url_prefix')        if url_prefix is None:            url_prefix = self.blueprint.url_prefix        self.url_prefix = url_prefix        self.url_defaults = dict(self.blueprint.url_values_defaults)        self.url_defaults.update(self.options.get('url_defaults', ()))    def add_url_rule(self, rule, endpoint=None, view_func=None, **options):                if self.url_prefix:            rule = self.url_prefix + rule        options.setdefault('subdomain', self.subdomain)        if endpoint is None:            endpoint = _endpoint_from_view_func(view_func)        defaults = self.url_defaults        if 'defaults' in options:            defaults = dict(defaults, **options.pop('defaults'))        self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint),                              view_func, defaults=defaults, **options)
s.add_url_rule方法中, 首先确定了self.url_prefix和endpoint, self.url_prefix就是我们创建Blueprint类对象时指定的prefix. 

最后调用了self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), view_func, defaults=defaults, **options), 可以发现还是通过app注册路由, 不过注册时的endpoint变成了'%s.%s' % (self.blueprint.name, endpoint), 就是说endpoint会被自动的在前面加上一个前缀.


总结:

Flask注册路由的功能来自werkzeug.routing模块的Rule和Map类, 对于每个注册的rule,  都会生成一个正则表达式与之匹配. 当客户端请求传来时,  会根据该正则表达式解析请求url. 如果是动态路由, 则会解析出若干参数, 传入到对应的视图函数中

Flask的Blueprint实际上会为每个注册的路由的endpoint加上前缀, 由此实现路由分类的功能