As a little programmer in my programming field, I am currently working as team lead in an entrepreneurial team. The technology stack involves Android, Python, Java and Go, which is also the main technology stack of our team. Contact: [email protected] wechat official account: angryCode
Pick up where I left off and read Flask’s source code to see how framework routing works.
0x00 Routing Principle
Let’s start with the simple use of Flask
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello(a):
return f'Hello, World! '
if __name__ == '__main__':
app.run()
Copy the code
Flask uses the @app.route decorator to map urls to methods.
Flask.route
Open the route method
def route(self, rule, **options):
""" The comments for this method are very detailed. To avoid too much code, the comments are omitted.
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator
Copy the code
There are two parameters in the route method: rule and options. Rule is a URL rule. The options parameter is mainly used by the werkzeug.routing. rule class. The method also defines decorator methods that store url path rules, method name mappings, and function method names and function objects in a dictionary.
Flask.add_url_rule
def add_url_rule(self, rule, endpoint, **options):
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',))
self.url_map.add(Rule(rule, **options))
Copy the code
The comments for this method are also very detailed, which roughly means if a method is defined
@app.route('/')
def index(a):
pass
Copy the code
Is equivalent to
def index(a):
pass
app.add_url_rule('index'.'/')
app.view_functions['index'] = index
Copy the code
Finally, the url_map.add method is called to construct the rule and option as rules and add them to a Map object.
Rule
Rule stands for URL Rule, which is a class defined in the Werkzeug library.
Url_map is a user-defined Map object. Its purpose is to achieve mapping between URLS and methods.
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):
rule.bind(self)
self._rules.append(rule)
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
Copy the code
The add method calls the bind method in Rule, where the actual binding logic is implemented.
Rule.bind
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))
The map is stored on the map property of the rule object itself
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
rule = self.subdomain + '|' + (self.is_leaf and self.rule or self.rule.rstrip('/'))
self._trace = []
self._converters = {}
self._weights = []
regex_parts = []
for converter, arguments, variable in parse_rule(rule):
if converter is None:
regex_parts.append(re.escape(variable))
self._trace.append((False, variable))
self._weights.append(len(variable))
else:
convobj = get_converter(map, converter, arguments)
regex_parts.append('(? P<%s>%s)' % (variable, convobj.regex))
self._converters[variable] = convobj
self._trace.append((True, variable))
self._weights.append(convobj.weight)
self.arguments.add(str(variable))
if convobj.is_greedy:
self.greediness += 1
if not self.is_leaf:
self._trace.append((False.'/'))
if not self.build_only:
regex = r'^%s%s$' % (
u''.join(regex_parts),
(not self.is_leaf or not self.strict_slashes) and \
'(?
/?) ' or ' '
)
self._regex = re.compile(regex, re.UNICODE)
Copy the code
The for loop in the bind method calls the parse_url method, which is a generator function that uses the re and yields back a tuple. There’s a lot of detail in this approach, but let’s take the main thread and get the whole process straight.
Flask starts with a decorator route that maps the URL to the function method that responds.
The call logic is
Flask.route -> Flask.add_url_rule -> Map.add -> Rule.bind
Copy the code
0x01 Responding to a Request
Flask will start a Web server by default after the service is started, which is easy for development and debugging. In the actual environment, tools such as Nginx + Gunicorn will be used for deployment. Since deployment is not the subject of this section, we will focus on how client requests are responded to.
In the previous article we learned that Flask started the service using the Run_simple method in the Werkzeug library.
This method is executed when the client sends a request
Flask.wsgi_app
def wsgi_app(self, environ, start_response):
"""The actual WSGI application. This is not implemented in `__call__` so that middlewares can be applied: app.wsgi_app = MyMiddleware(app.wsgi_app) :param environ: a WSGI environment :param start_response: a callable accepting a status code, a list of headers and an optional exception context to start the response """
with self.request_context(environ):
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
response = self.make_response(rv)
response = self.process_response(response)
return response(environ, start_response)
Copy the code
Request_context (environ) creates a request context instance. Preprocess_request is preprocessed and dispatch_request is sent. The responses make_response and process_response are then executed, and response is returned.
Here we focus on dispatch_request.
Flask.dispatch_request
def dispatch_request(self):
"""Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. """
try:
endpoint, values = self.match_request()
return self.view_functions[endpoint](**values)
except HTTPException as e:
handler = self.error_handlers.get(e.code)
if handler is None:
return e
return handler(e)
except Exception as e:
handler = self.error_handlers.get(500)
if self.debug or handler is None:
raise
return handler(e)
Copy the code
At the heart of this method is match_request, which matches the url rules requested by the client to find the corresponding function method.
Flask.match_request
def match_request(self):
"""Matches the current request against the URL map and also stores the endpoint and view arguments on the request object is successful, otherwise the exception is stored. """
rv = _request_ctx_stack.top.url_adapter.match()
request.endpoint, request.view_args = rv
return rv
Copy the code
When the match is complete, self.view_functions[endpoint](**values) is called to execute the corresponding function method and return the return value of the function.
If the dispatch_request does not match the URL rule, the error_Handlers dictionary is used to find the corresponding error code and the handler method is executed.
The matching process of URL routing rules is complete.
0x02 to sum up
After Flask is started, the route decorator is parsed and the URL rules and function methods are saved. The flask.wsgi_app method is executed at the request of the client and starts matching the URL to find the corresponding method, which is returned after execution.
0x03 Learning Materials
- werkzeug.palletsprojects.com/en/0.15.x/
- palletsprojects.com/p/flask/
- Docs.python.org/3/library/h…