Table of contents
Source series:
Flask source code articles: wsgi, Werkzeug and Flask startup workflow
Flask source code article: 2w words thoroughly understand Flask is the context principle
If you don't want to read the specific analysis process, you can read the summary directly, and you can understand it!
1 Routing-related operations at startup
The so-called routing principle is how Flask creates its own routing system, and when a request comes, how to accurately locate the processing function according to the routing system and respond to the request.
This section uses the simplest Flask application example to explain the routing principle, as follows:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
(1) Analyze app.route()
route()
First, the registration of the route is realized through the decorator under the scaffold (Flask inherits the scaffold) package , and its source code is as follows:
def route(self, rule: str, **options: t.Any) -> t.Callable:
def decorator(f: t.Callable) -> t.Callable:
# 获取endpoint
endpoint = options.pop("endpoint", None)
# 添加路由,rule就是app.route()传来的路由字符串,及'/'
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
You can see that this decorator mainly does two things: 1. Obtain endpoint
; 2. Add routing.
The most important of these is the function add_url_rule()
, which is used to add route mappings.
Replenish:
- The endpoint is used later when Flask stores routing and function name mappings. If not specified, the default is the decorated function name. How to use it will be analyzed later;
- Because the essence of app.route() is still the add_url_rule() function, we can also use this function directly. For usage, please refer to the article Flask routing .
(2) Analyze add_url_rule()
Let's take a look at what add_url_rule does. Its core source code is as follows:
class Flask(Scaffold):
# 这里只有要讨论的主要代码,其他代码省略了
url_rule_class = Rule
url_map_class = Map
def __init__(
self,
import_name: str,
static_url_path: t.Optional[str] = None,
static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
static_host: t.Optional[str] = None,
host_matching: bool = False,
subdomain_matching: bool = False,
template_folder: t.Optional[str] = "templates",
instance_path: t.Optional[str] = None,
instance_relative_config: bool = False,
root_path: t.Optional[str] = None,
):
super().__init__(
import_name=import_name,
static_folder=static_folder,
static_url_path=static_url_path,
template_folder=template_folder,
root_path=root_path,
)
self.url_map = self.url_map_class()
self.url_map.host_matching = host_matching
self.subdomain_matching = subdomain_matching
def add_url_rule(
self,
rule: str,
endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None,
provide_automatic_options: t.Optional[bool] = None,
**options: t.Any,
) -> None:
# 1.如果没提供endpoint,获取默认的endpoint
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func) # type: ignore
options["endpoint"] = endpoint
# 2.获取请求方法,在装饰器@app.route('/index', methods=['POST','GET'])的method参数
# 如果没有指定,则给个默认的元组("GET",)
# 关于provide_automatic_options处理一些暂不看
methods = options.pop("methods", None)
if methods is None:
methods = getattr(view_func, "methods", None) or ("GET",)
if isinstance(methods, str):
raise TypeError(
"Allowed methods must be a list of strings, for"
' example: @app.route(..., methods=["POST"])'
)
methods = {
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
# 3.重要的一步:url_rule_class方法实例化Rule对象
rule = self.url_rule_class(rule, methods=methods, **options)
rule.provide_automatic_options = provide_automatic_options # type: ignore
# 4.重要的一步:url_map(Map对象)的add方法
self.url_map.add(rule)
# 5.判断endpoint和view_func的映射存不存在,如果已经有其他view_func用了这个endpoint,则报错,否则新的映射加到self.view_functions里
# self.view_functions继承自Scaffold,是一个字典对象
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"
f" endpoint function: {
endpoint}"
)
self.view_functions[endpoint] = view_func
Analyzing the above source code, this method mainly does the following things:
- If not provided
endpoint
, get the defaultendpoint
; @app.route('/index', methods=['POST','GET'])
Get the request method, in the parameters of the decoratormethod
, if not specified, give a default tuple ("GET",);self.url_rule_class()
: InstantiateRule
the object, which will be explained later in the Rule class;- Call
self.url_map.add()
method, where self.url_map is anMap
object, which will be explained later; self.view_functions[endpoint] = view_func
Add the mapping of endpoint and view_func.
Among them, instantiating Rule
objects and sums self.url_map.add()
is the core of Falsk routing. The following analyzes Rule
classes and Map
classes.
(3) Analyze the Rule class
Rule
The class is werkzeug.routing
under the module, and its source code is more. Here we only extract the main code we use, as follows:
class Rule(RuleFactory):
def __init__(
self,
string: str,
methods: t.Optional[t.Iterable[str]] = None,
endpoint: t.Optional[str] = None,
# 此处省略了其他参数的代码 ...
) -> 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: "Map" = None # type: ignore
self.methods = methods
self.endpoint: str = endpoint
# 省略了其他初始化的代码
def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
"""获取map对象的rule迭代器"""
yield self
def bind(self, map: "Map", rebind: bool = False) -> None:
"""把map对象绑定到Rule对象上,并且根据rule和map信息创建一个path正则表达式,存储在rule对象的self._regex属性里,路由匹配的时候用"""
if self.map is not None and not rebind:
raise RuntimeError(f"url rule {
self!r} already bound to map {
self.map!r}")
# 把map对象绑定到Rule对象上
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
# 调用compile方法创建一个正则表达式
self.compile()
def compile(self) -> None:
"""编写正则表达式并存储到属性self._regex中"""
# 此处省略了正则的解析过程代码
regex = f"^{
''.join(regex_parts)}{
tail}$"
self._regex = re.compile(regex)
def match(
self, path: str, method: t.Optional[str] = None
) -> t.Optional[t.MutableMapping[str, t.Any]]:
"""这个函数用于校验传进来path参数(路由)是否能够匹配,匹配不上返回None"""
# 省去了部分代码,只摘录了主要代码,看一下大致逻辑即可
if not self.build_only:
require_redirect = False
# 1.根据bind后的正则结果(self._regex正则)去找path的结果集
m = self._regex.search(path)
if m is not None:
groups = m.groupdict()
# 2.编辑匹配到的结果集,加到一个result字典里并返回
result = {
}
for name, value in groups.items():
try:
value = self._converters[name].to_python(value)
except ValidationError:
return None
result[str(name)] = value
if self.defaults:
result.update(self.defaults)
return result
return None
Rule
The class inherits from RuleFactory
the class, the main parameters are:
string
: route stringmethods
: route methodendpoint
:endpoint parameter
A Rule instance represents a URL pattern, and a WSGI application will process many different URL patterns, and at the same time generate many Rule instances, and these instances will be passed as parameters to the Map class.
(4) Analyze the Map class
Map
The class is also under the werkzeug.routing module, and there are many source codes. Here we only extract the main codes we use. The main source codes are as follows:
class Map:
def __init__(
self,
rules: t.Optional[t.Iterable[RuleFactory]] = None
# 此处省略了其他参数
) -> None:
# 根据传进来的rules参数维护了一个私有变量self._rules列表
self._rules: t.List[Rule] = []
# endpoint和rule的映射
self._rules_by_endpoint: t.Dict[str, t.List[Rule]] = {
}
# 此处省略了其他初始化操作
def add(self, rulefactory: RuleFactory) -> None:
"""
把Rule对象或一个RuleFactory对象添加到map并且绑定到map,要求rule没被绑定过
"""
for rule in rulefactory.get_rules(self):
# 调用rule对象的bind方法
rule.bind(self)
# 把rule对象添加到self._rules列表里
self._rules.append(rule)
# 把endpoint和rule的映射加到属性self._rules_by_endpoint里
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
def bind(
self,
server_name: str,
script_name: t.Optional[str] = None,
subdomain: t.Optional[str] = None,
url_scheme: str = "http",
default_method: str = "GET",
path_info: t.Optional[str] = None,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
) -> "MapAdapter":
"""
返回一个新的类MapAdapter
"""
server_name = server_name.lower()
if self.host_matching:
if subdomain is not None:
raise RuntimeError("host matching enabled and a subdomain was provided")
elif subdomain is None:
subdomain = self.default_subdomain
if script_name is None:
script_name = "/"
if path_info is None:
path_info = "/"
try:
server_name = _encode_idna(server_name) # type: ignore
except UnicodeError as e:
raise BadHost() from e
return MapAdapter(
self,
server_name,
script_name,
subdomain,
url_scheme,
path_info,
default_method,
query_args,
)
Map
Classes have two very important attributes:
self._rules
, the attribute is a list that stores a series of Rule objects;self._rules_by_endpoint
:
There is a core method add()
, here is our analysis app.add_url_rule()
method is the method called in step 4. It will be explained in detail later.
(5) Analyze the MapAdapter class
In Map
the class, MapAdapter
the class will be used. Let's get to know this class:
class MapAdapter:
"""`Map.bind`或`Map.bind_to_environ` 会返回这个类
主要用来做匹配
"""
def match(
self,
path_info: t.Optional[str] = None,
method: t.Optional[str] = None,
return_rule: bool = False,
query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None,
websocket: t.Optional[bool] = None,
) -> t.Tuple[t.Union[str, Rule], t.Mapping[str, t.Any]]:
"""匹配请求的路由和Rule对象"""
# 只摘摘录了主要代码,省略了大量代码...
# 这里是主要步骤:遍历map对象的rule列表,依次和path进行匹配
for rule in self.map._rules:
try:
# 调用rule对象的match方法返回匹配结果
rv = rule.match(path, method)
except RequestPath as e:
# 下面省略了大量代码...
# 返回rule对象(或endpoint)和匹配的路由结果
if return_rule:
return rule, rv
else:
return rule.endpoint, rv
Map.bind
or Map.bind_to_environ
method returns MapAdapter
an object.
MapAdapter
The core method of the object is that the match
main step is to traverse the rule list of the map object and match it with the path in turn. Of course, the match method of the rule object is called to return the matching result.
Map
Next, you can look at how classes and objects are combined to use a little independently Rlue
. See the following example:
from werkzeug.routing import Map, Rule
m = Map([
Rule('/', endpoint='index'),
Rule('/blog', endpoint='blog/index'),
Rule('/blog/<int:id>', endpoint='blog/detail')
])
# 返回一个MapAdapter对象
map_adapter = m.bind("example.com", "/")
# MapAdapter对象的 match方法会返回匹配的结果
print(map_adapter.match("/", "GET"))
# ('index', {})
print(map_adapter.match("/blog/42"))
# ('blog/detail', {'id': 42})
print(map_adapter.match("/blog"))
# ('blog/index', {})
It can be seen that Map
the object bind
returns an MapAdapter
object, and the method MapAdapter
of the object match
can find the matching result of the route.
(6) Analyze url_rule_class()
add_url_rule
The first major step is rule = self.url_rule_class(rule, methods=methods, **options)
to create an Rule
object.
When analyzing Rlue
the class, I know that the Rule object mainly has string
(routing string), methods
, and endpoint
3 attributes. Take a concrete example below to see what the instantiated Rule
object looks like.
Still the top example at the beginning, let’s look at @app.route('/')
the specific properties of the object when the Rule object is instantiated after passing the code, as follows through debugging:
It can be seen that rule
the attribute of the rule object is the passed route, endpoint
the attribute is obtained through the function name, method
the attribute is the supported request method, and 'HEAD' OPTIONS
is added by default.
(7) Analysis map.add(rule)
add_url_rule
The second major step is self.url_map.add(rule)
to call Map
the object's add method.
When analyzing the Map object in step 4, it was mentioned, now let's go back and take a closer look at what this method does:
def add(self, rulefactory: RuleFactory) -> None:
"""
把Rule对象或一个RuleFactory对象添加到map并且绑定到map,要求rule没被绑定过
"""
for rule in rulefactory.get_rules(self):
# 调用rule对象的bind方法
rule.bind(self)
# 把rule对象添加到self._rules列表里
self._rules.append(rule)
# 把endpoint和rule的映射加到属性self._rules_by_endpoint里
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
In fact, the main thing is to add the Rule object or a RuleFactory object instantiated in the previous step to the Map object and bind it to the map.
Mainly did two things:
- Call the bind method of the rule object
rule.bind(self)
: When analyzing the Rule class, this method was mentioned. Its main function is to bind the map object to the Rule object, and create a regular expression (the property of the Rule object) based on the rule and map informationself._regex
. . - Add the rule object to the list
Map
of objectsself._rules
; - Add the mapping of
endpoint
and to the object's properties (a dictionary);rule
Map
self._rules_by_endpoint
We can use an example to see what the Map object becomes after adding. Through debugging, the results are as follows:
It can be seen that Map
both the self._rules
and self._rules_by_endpoint
attributes contain the data corresponding to the newly added '/' route (the /static/<path:filename>
route is the location route of the static file added by default).
The above analysis is over, how to add a routing map.
2 The route matching process when the request comes in
(1) Analyze wsgi_app
Let's analyze how to match the route according to the previous request Map
and the object when the previous request comes in .Rlue
In the previous chapter, we analyzed __call__
the method of invoking the Flask app after processing the request through the wsgi server when the request comes in. The code is as follows:
def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app`, which can be
wrapped to apply middleware.
"""
return self.wsgi_app(environ, start_response)
You can see that the method of the app is actually called wsgi_app()
. Its code is as follows:
def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
# 1.获取请求上下文
ctx = self.request_context(environ)
error: t.Optional[BaseException] = None
try:
try:
# 2.调用请求上下文的push方法
ctx.push()
# 3.调用full_dispatch_request()分发请求,获取响应结果
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
There are 3 main steps in it:
- Get request context:
ctx = self.request_context(environ)
- Call the push method of the request context:
ctx.push()
- Call full_dispatch_request() to distribute the request and get the response result:
response = self.full_dispatch_request()
Let's analyze the function of each step one by one.
(2) Analyze request_context
wsgi_app
The first major step of the method.
This method is mainly to obtain a context object.
Need to pass environ
(environment variables, etc.) into the method. Context is also an important concept in Flask. Of course, the next chapter will focus on the analysis of context. This chapter only focuses on what we need.
def request_context(self, environ: dict) -> RequestContext:
return RequestContext(self, environ)
This method is to create an RequestContext
object. RequestContext
The source code of the class part is as follows:
class RequestContext:
def __init__(
self,
app: "Flask",
environ: dict,
request: t.Optional["Request"] = None,
session: t.Optional["SessionMixin"] = None,
) -> None:
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = None
try:
# 此处是重点,调用了Falsk对象的create_url_adapter方法获取了MapAdapter对象
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
self.flashes = None
self.session = session
# 其他代码省略...
# 其他方法的源码省略...
In the initialization method of creating RequestContext
an object, a very important step is to obtain MapAdapter
the object .
We have analyzed its function in Section 4 above, and it is mainly used to match routes.
Let's look at create_url_adapter
the source code:
def create_url_adapter(
self, request: t.Optional[Request]
) -> t.Optional[MapAdapter]:
if request is not None:
if not self.subdomain_matching:
subdomain = self.url_map.default_subdomain or None
else:
subdomain = None
# 此处是重点,调用了Map对象的bind_to_environ方法
return self.url_map.bind_to_environ(
request.environ,
server_name=self.config["SERVER_NAME"],
subdomain=subdomain,
)
if self.config["SERVER_NAME"] is not None:
# 此处是重点,调用了Map对象的bind方法
return self.url_map.bind(
self.config["SERVER_NAME"],
script_name=self.config["APPLICATION_ROOT"],
url_scheme=self.config["PREFERRED_URL_SCHEME"],
)
return None
As you can see, the main function of this method is to call the method or method Map
of the object . I also analyzed it when I talked about the Map class earlier. These two methods mainly return objects.bind_to_environ
bind
MapAdapter
(3) Analyze ctx.push
wsgi_app
The second main step of the method.
After the context object is obtained in the wsgi method, the method is called push
, and the code is as follows (only the core code is kept):
class RequestContext:
def __init__(
self,
app: "Flask",
environ: dict,
request: t.Optional["Request"] = None,
session: t.Optional["SessionMixin"] = None,
) -> None:
# 代码省略
pass
def match_request(self) -> None:
try:
# 1.调用了MapAdapter对象的match方法,返回了rule对象和参数对象
result = self.url_adapter.match(return_rule=True)
# 2.把rule对象和参数对象放到请求上下文中
self.request.url_rule, self.request.view_args = result
except HTTPException as e:
self.request.routing_exception = e
def push(self) -> None:
"""Binds the request context to the current context."""
# 此处省略了前置校验处理代码(上下文、session等处理)
if self.url_adapter is not None:
# 调用了match_request方法
self.match_request()
It can be seen push
that the method mainly calls match_request
the method. This method mainly does the following two things:
- Calling the match method of the MapAdapter object will
Map
match the route of the current request according to the routing information stored in the object, and return the rule object and parameter object. - Put the rule object and parameter object into the request context.
(4) Analyze full_dispatch_request
wsgi_app
The third main step of the method.
full_dispatch_request
The method source code is as follows:
def full_dispatch_request(self) -> Response:
self.try_trigger_before_first_request_functions()
try:
request_started.send(self)
rv = self.preprocess_request()
if rv is None:
# 这里是主要的步骤:分发请求
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
return self.finalize_request(rv)
Among them dispatch_request()
is the core method.
dispatch_request()
The method source code is as follows:
def dispatch_request(self) -> ResponseReturnValue:
# 1.获取请求上下文对象
req = _request_ctx_stack.top.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
# 2.从上下文中获取前面存在里面的Rule对象
rule = req.url_rule
# if we provide automatic options for this URL and the
# request came with the OPTIONS method, reply automatically
if (
getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS"
):
return self.make_default_options_response()
# 这里是重点:根据rule对象的endpoint属性从self.view_functions属性中获取对应的视图函数,
# 然后把上下文中的参数传到视图函数中并调用视图函数处理请求,返回处理结果
return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
The main steps of this method are as follows:
req = _request_ctx_stack.top.request
: Get the request context objectrule = req.url_rule
: Obtain the previously existingRule
object from the context, which isctx.push()
the method put into the context- Obtain the corresponding view function from the self.view_functions property according to the endpoint property of the rule object, then pass the parameters in the context to the view function and call the view function to process the request, and return the processing result:
return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
At this point, a complete request is processed.
3 Summary
According to the above analysis, the routing rules and request matching are summarized as follows:
When the app starts:
app.route()
calladd_url_rule
method.add_url_rule
In the method: call theself.url_rule_class()
instantiatedRule
object;add_url_rule
In the method: callself.url_map.add()
the method, and store the mapping relationship betweenRule
the objectendpoint
and the view function in the Map object.add_url_rule
In the method:self.view_functions[endpoint] = view_func
add the mapping of endpoint and view_func.
Request matching process:
- When the request comes in,
WSGI
the server processes and calls__call__
the method of the Flask app, and then callswsgi_app
the method; wsgi_app
Create a context object in the method:ctx = self.request_context(environ)
. Objects are then instantiatedMapAdapter
as context object properties;wsgi_app
The method of calling the context object in the methodpush
:ctx.push()
. This method mainly uses the methodsMapAdapter
of the objectmatch
.MapAdapter
The method of the objectmatch
, to call the methodrule
of the objectmatch
. This methodMap
matches the route of the current request according to the routing information stored in the object, and getsrule
the object and parameter object into the context object.wsgi_app
Callfull_dispatch_request
the method in the method, and then calldispatch_request()
the method in it;dispatch_request()
In the method: get the request context object, get the object insideRule
, get the corresponding view function from the attribute according to the endpoint attribute of the rule objectself.view_functions
, then pass the parameters in the context to the view function and call the view function to process the request, and return the processing result .
The whole flow chart is as follows:
In the first half of the project, how to use Rule
and Map
object to establish routing rules; the second half is how to use routing rules to match when requests come in.