flask_python_web框架源码阅读(2)路由映射是如何生成的

接着上一篇flask是如何启动的,这篇主要讲路由映射是何时生成的,以及生成过程是如何的,如果有写的不太准确的地方,麻烦帮忙指正,谢谢,希望大家一起进步。

文章目录

  • 简单示例
  • 源码部分
    • app.route源码阅读
      • 装饰器使用相关
    • app.add_url_rule源码
    • Map.add源码
    • Rule.bind源码
    • Rule.compile源码
    • Rule._compile_builder源码
  • Tips
    • 增加对路由理解的辅助源码

简单示例

我们都知道,flask的路由匹配是基于python特有语法装饰器实现的,如下代码所示

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

if __name__ == '__main__':
    app.run()

源码部分

app.route源码阅读

  • 那么@app.route(’/’)是如何实现url和试图函数之间的映射的,我们来看下源码
def route(self, rule, **options):
    """看这几个参数的解释就行了,其他的解释都是使用帮助"""
    :param rule: the URL rule as string
    :param methods: a list of methods this rule should be limited
                        to (``GET``, ``POST`` etc.).  By default a rule
                        just listens for ``GET`` (and implicitly ``HEAD``).
    :param subdomain: specifies the rule for the subdoain in case
                          subdomain matching is in use.
    :param strict_slashes: can be used to disable the strict slashes
                               setting for this rule.  See above.
    :param options: other options to be forwarded to the underlying
                        :class:`~werkzeug.routing.Rule` object.
        """
    def decorator(f):
        self.add_url_rule(rule, f.__name__, **options)
        self.view_functions[f.__name__] = f
        return f
    return decorator

装饰器使用相关

0x00

要想看懂这个,得了解点装饰器相关的用法,这里简单介绍一下,这里主要是说针对函数的装饰器

首先大家必须要了解的是,装饰器函数是在被加载时就已经运行了,而不是等到__main__函数确定调用到使用装饰器的地方才开始执行装饰器,举个例子说

python的脚本文件,大伙都知道,是从上往下运行的,碰到def或是class是不运行函数内容的,但是如果碰到@decrator这种,就会直接执行decroator(func)这个函数,当然func(*args)是不执行的

0x01

结构最简单的装饰器(不带参数)

# 最最常见的装饰器,作用是给被包装的函数加一些功能,比如计时啊,日志相关啊等等
import time
from functools import wraps

# 装饰器函数,参数为要被装饰的函数
def calc_time(func):
    '''
    Decorator that reports the execution time.
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        # 执行原函数
        result = func(*args, **kwargs)
        end = time.time()
        # 所添加的功能就是计算被装饰函数的运行时间
        print(func.__name__, end-start)
        return result
    return wrapper

# 使用该装饰器,计算这个函数用时多久
@timethis
def countdown(n):
    pass

# 上面的装饰器写法等同于下面的执行过程
def countdown(n):
    pass
countdown = timethis(countdown)

0x02

和Flask.route相关的复杂一点的装饰器(带参数)

# 通过观察,可以看得出来,不带参数的装饰器函数只需要嵌套一层内部函数就行了
# 而带参数的装饰器函数,则需要嵌套两层,最外面一层是传递的参数,进行一些操作,第二层是传递被装饰的函数
# 第三层则是正常的被装饰函数的原有参数信息
from functools import wraps
import logging

# 外部参数在这里进行传递
def logged(level, name=None, message=None):
    # 传递被装饰函数给内部装饰器
    def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        # 执行被装饰函数的地方
        def wrapper(*args, **kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
    return decorate

# 就可以传递参数,通过参数的判断,击行函数的执行
# Example use
@logged(logging.DEBUG)
def add(x, y):
    return x + y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')
  1. 了解了这两种常见的装饰器模型,其实就可以理解flask中的route函数就是其中第二种的套用,不同的是,只写到了第二层,返回要装饰的的函数,只有客户端访问相应的路由,才会调用到视图函数
  2. 我们可以看出decorator函数中只调用了一个核心函数add_url_rule(),我们接下来看下这个函数都干了什么

app.add_url_rule源码

还是先看源码长什么样子

def add_url_rule(self, rule, endpoint, **options):
        """Connects a URL rule.  Works exactly like the :meth:`route`
        decorator but does not register the view function for the endpoint.

        Basically this example::

            @app.route('/')
            def index():
                pass

        Is equivalent to the following::

            def index():
                pass
            app.add_url_rule('index', '/')
            app.view_functions['index'] = index

        :param rule: the URL rule as string
        :param endpoint: the endpoint for the registered URL rule.  Flask
                         itself assumes the name of the view function as
                         endpoint
        :param options: the options to be forwarded to the underlying
                        :class:`~werkzeug.routing.Rule` object
        """
        options['endpoint'] = endpoint
        options.setdefault('methods', ('GET',))
        self.url_map.add(Rule(rule, **options))

注释解释的比较详细了,将装饰器和这个函数的关系解释的明明白白

  1. endpoint是路由名称,就是你在后端设置路由函数的名字,通过这个名字可以直接找得到相应的路由函数,默认为路由函数的名字
  2. methods表示请求方式,默认为get
  3. 上述两个参数都是为调用werkzeug的路由库Routing.Rule使用的
  4. url_map.add(Rule(rule, **options))
    • 可以看的出来这是往路由映射中添加Rule对象
    • Rule是werkzeug的路由库的类,这里面映射着端点名,路由以及视图函数的映射关系
    • 接下来又要看add方法干什么活了

Map.add源码

这是Map的add方法,所以这里的self是指map对象,而for循环中的rule是Rule对象,所以rule.bind是要从Rule中寻找bind方法去研究,为什么这里说一下,因为Map类中也有个add方法,别看错了

    def add(self, rulefactory):
        """Add a new rule or factory to the map and bind it.  Requires that the
        rule is not bound to another map.

        :param rulefactory: a :class:`Rule` or :class:`RuleFactory`
        """
        for rule in rulefactory.get_rules(self):
            # self是指的当前的map对象
            rule.bind(self)
            self._rules.append(rule)
            self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
        self._remap = True

Rule.bind源码

绑定路由到url_map函数

    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))
        # 将所有规则全部放到一个map中
        self.map = map
        # 取一些必须的默然默认值
        if self.strict_slashes is None:
            self.strict_slashes = map.strict_slashes
        if self.merge_slashes is None:
            self.merge_slashes = map.merge_slashes
        if self.subdomain is None:
            self.subdomain = map.default_subdomain。
        # 这个函数复杂,下面有
        self.compile()

Rule.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._static_weights = []
        self._argument_weights = []
        regex_parts = []

        def _build_regex(rule):
            index = 0
            # 转换器 (converter)、转换器参数(argument) 和名称(name)
            for converter, arguments, variable in parse_rule(rule):
                if converter is None:
                    for match in re.finditer(r"/+|[^/]+", variable):
                        part = match.group(0)
                        if part.startswith("/"):
                            if self.merge_slashes:
                                regex_parts.append(r"/+?")
                                self._trace.append((False, "/"))
                            else:
                                regex_parts.append(part)
                                self._trace.append((False, part))
                            continue
                        self._trace.append((False, part))
                        regex_parts.append(re.escape(part))
                        if part:
                            self._static_weights.append((index, -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._argument_weights.append(convobj.weight)
                    self.arguments.add(str(variable))
                index = index + 1

        _build_regex(domain_rule)
        regex_parts.append("\\|")
        self._trace.append((False, "|"))
        _build_regex(self.rule if self.is_leaf else self.rule.rstrip("/"))
        if not self.is_leaf:
            self._trace.append((False, "/"))

        self._build = self._compile_builder(False).__get__(self, None)
        self._build_unknown = self._compile_builder(True).__get__(self, None)

        if self.build_only:
            return

        if not (self.is_leaf and self.strict_slashes):
            reps = u"*" if self.merge_slashes else u"?"
            tail = u"(?/%s)" % reps
        else:
            tail = u""

        regex = u"^%s%s$" % (u"".join(regex_parts), tail)
        self._regex = re.compile(regex, re.UNICODE)

Rule._compile_builder源码

这个函数是将路由映射完成的核心函数,比较复杂,可以打断点走着看

def _compile_builder(self, append_unknown=True):
        defaults = self.defaults or {}
        dom_ops = []
        url_ops = []

        opl = dom_ops
        for is_dynamic, data in self._trace:
            if data == "|" and opl is dom_ops:
                opl = url_ops
                continue
            # this seems like a silly case to ever come up but:
            # if a default is given for a value that appears in the rule,
            # resolve it to a constant ahead of time
            if is_dynamic and data in defaults:
                data = self._converters[data].to_url(defaults[data])
                opl.append((False, data))
            elif not is_dynamic:
                opl.append(
                    (False, url_quote(to_bytes(data, self.map.charset), safe="/:|+"))
                )
            else:
                opl.append((True, data))

        def _convert(elem):
            ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
            ret.args = [ast.Name(str(elem), ast.Load())]  # str for py2
            return ret

        def _parts(ops):
            parts = [
                _convert(elem) if is_dynamic else ast.Str(s=elem)
                for is_dynamic, elem in ops
            ]
            parts = parts or [ast.Str("")]
            # constant fold
            ret = [parts[0]]
            for p in parts[1:]:
                if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
                    ret[-1] = ast.Str(ret[-1].s + p.s)
                else:
                    ret.append(p)
            return ret

        dom_parts = _parts(dom_ops)
        url_parts = _parts(url_ops)
        if not append_unknown:
            body = []
        else:
            body = [_IF_KWARGS_URL_ENCODE_AST]
            url_parts.extend(_URL_ENCODE_AST_NAMES)

        def _join(parts):
            if len(parts) == 1:  # shortcut
                return parts[0]
            elif hasattr(ast, "JoinedStr"):  # py36+
                return ast.JoinedStr(parts)
            else:
                call = _prefix_names('"".join()')
                call.args = [ast.Tuple(parts, ast.Load())]
                return call

        body.append(
            ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
        )

        # str is necessary for python2
        pargs = [
            str(elem)
            for is_dynamic, elem in dom_ops + url_ops
            if is_dynamic and elem not in defaults
        ]
        kargs = [str(k) for k in defaults]

        func_ast = _prefix_names("def _(): pass")
        func_ast.name = "".format(self.rule)
        if hasattr(ast, "arg"):  # py3
            func_ast.args.args.append(ast.arg(".self", None))
            for arg in pargs + kargs:
                func_ast.args.args.append(ast.arg(arg, None))
            func_ast.args.kwarg = ast.arg(".kwargs", None)
        else:
            func_ast.args.args.append(ast.Name(".self", ast.Param()))
            for arg in pargs + kargs:
                func_ast.args.args.append(ast.Name(arg, ast.Param()))
            func_ast.args.kwarg = ".kwargs"
        for _ in kargs:
            func_ast.args.defaults.append(ast.Str(""))
        func_ast.body = body

        # use `ast.parse` instead of `ast.Module` for better portability
        # python3.8 changes the signature of `ast.Module`
        module = ast.parse("")
        module.body = [func_ast]

        # mark everything as on line 1, offset 0
        # less error-prone than `ast.fix_missing_locations`
        # bad line numbers cause an assert to fail in debug builds
        for node in ast.walk(module):
            if "lineno" in node._attributes:
                node.lineno = 1
            if "col_offset" in node._attributes:
                node.col_offset = 0

        code = compile(module, "", "exec")
        return self._get_func_code(code, func_ast.name)

Tips

增加对路由理解的辅助源码

为了加深理解,建议用如下代码打断点,走一走我重点标注的这几个函数

from werkzeug.routing import Map, Rule

url_rules = [
    Rule('/', endpoint='index', methods=['GET']),
    Rule('/user', endpoint='user', methods=['GET'])
]

map = Map(url_rules)

print(map)

将url路由映射关系添加整理为小demo如下

from flask import Flask
from werkzeug.routing import Map, Rule

app = Flask(__name__)

def index():
    return 'hello, world'

def user():
    return 'hello, stranger'

# 假设映射关系是这样,需要自己构建
url_rules = [
    Rule('/', endpoint='index', methods=['GET']),
    Rule('/user', endpoint='user', methods=['GET'])
]
map_list = Map(url_rules)

# 将app和关联起来,其实就是Flask关于路由的属性需要设置
app.url_map = map_list
app.view_functions = {
    'index': index,
    'user': user
}

if __name__ == '__main__':
    app.run()

到此结束,总结来看,从Flask启动读到脚本中装饰器route就开始就把把url和响应的函数方法对应起来,从而应对客户端对于不同路由的响应

你可能感兴趣的:(web开发)