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 decoratorapp.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 resultapp中的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加上前缀, 由此实现路由分类的功能
- Flask源码解读(3) -- route
- flask -- route修饰器源码
- 解读flask框架,flask源码解读
- Flask源码解读(2) -- context
- Flask源码解读 <1> --- 浅谈Flask基本工作流程
- 简单实现flask route
- Flask 路由 route
- python-flask-route
- Flask和@app.route
- Flask源码解读 <2> --- 请求上下文和request对象
- flask-cache 缓存Jinja2模板之源码解读
- Flask源码解读(1) -- app.run()的背后
- Flask-HelloWorld-09 @app.route
- route print解读
- 解读route命令
- CppUnit源码解读(3)
- CppUnit源码解读(3)
- CYYMysql 源码解读 3
- 炒股不用愁,AI炒股神器帮你搞定所有难题!
- 14亿份明文登陆凭证暴露,个人与企业都可能遭殃
- java设计模式之单例
- linux目录操作
- vue之路
- Flask源码解读(3) -- route
- 面试
- Java 8新特性终极指南
- https://www.cnblogs.com/by-dream/p/5611555.html
- mysql分表和表分区详解
- 【上海线下】FMI2017人工智能系列沙龙-解读神秘GPU
- 2017年不断更新(迭代)的前端框架有哪些
- CentOS 7.4 64 yum安装MySQL5.7+Nginx1.12+PHP7.1
- USB接口知识学习