This paper briefly analyzes the source code of Flask, mainly focusing on WSGI, Flask object data structure, Flask application startup process, request processing process, view function, URL mapping, application context and request context. We won’t cover all of these topics, but explore them for your own reading of the source code. To follow this article, you need to be familiar with Flask, have written a small project in Flask, have some ability to read code, and have a basic understanding of what web frameworks can do.

This article will be updated from time to time. Last updated: September 10, 2017.

Flask’s official Demo code:

from flask import Flask
app = Flask(__name__)

@ app. The route ('/')
def index(a):
    return'Hello, world! 'if__name__ = = "__main__" : app. The run ()Copy the code

This article starts with this simple code and gives a brief introduction to WSGI, the data structure of Flask objects, the Flask application startup process, request processing, view functions, mapping of urls, request and Response classes (application context and request context), These topics cover the heart of a Web framework.

WSGI

When a user-initiated request arrives at the server, it is received by an HTTP server and handed to a Web application for business processing. This requires an interface between the HTTP server and the Web application. In the Python Web development world, Python officially approved this interface and named it WSGI, as specified by PEP333. As long as both servers and frameworks follow this convention, any combination of server and framework can be achieved. According to this specification, a WSGi-oriented framework must implement this method:

def application(environ, start_response)
Copy the code

The HTTP server calls the above method, passing in the environ dictionary and the start_response function, fetching the request information from environ, and then calling start_Response to set the response header. And returns the response body (which must be an traversable object, such as a list or dictionary) to the HTTP server, which in turn returns the response to the user.

Flask, as a Web framework for developing Web applications, solves the following problems:

  1. For an application to be able to be called by an HTTP server, it must have__call__methods
  2. Through the incoming request information (URL, HTTP method, and so on), find the correct business processing logic, that is, the correct view function
  3. Handles business logic, which might include form checking, database CRUD, and so on (which is not covered in this article)
  4. Returns the correct response
  5. When processing multiple requests at the same time, you also need to protect those requests and know which response to match which request, namely thread protection

Here’s how Flask solves these problems.

This series of articles gives you a basic understanding of how web servers and frameworks work together through WSGI.

Application Creation

Flask class in app.py.

The second line of the Demo code creates an instance of the Flask class, passing in the name of the current module. Let’s take a look at what a Flask application is and what its data structure is.

Flask is a class like this:

The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for the view functions, the URL rules, template configuration and much more.

The name of the package is used to resolve resources from inside the package or the folder the module is contained in depending on if the package parameter resolves to an actual python package (a folder with an __init__.py file inside) or a standard module (just a .py file).

A Flask object is actually a WSGI application. It accepts the name of a module or package as an argument. After it is created, all view functions, URL rules, template Settings, and so on are registered with it. The name of the module or package is passed in to locate some resources.

The Flask class has the following properties:

  • request_class = RequestSets the type of request
  • response_class = ResponseSets the type of response

Both types are derived from its dependency library, Werkzeug, and are simply extended.

The Flask object’s __init__ method is as follows:

def __init__(self, package_name):
    Flask objects have a dictionary that holds all the view functions
    self.view_functions = {}

    This dictionary is used to store all error-handling view functions
    #: dictionary keys are error type codes
    self.error_handlers = {}

    #: This list is used to hold functions that should be executed before the request is dispatched
    self.before_request_funcs = []

    #: Function that should be executed when the first request is received
    self.before_first_request_funcs = []

    #: The functions in this list are called after the request completes, and the response object is passed to these functions
    self.after_request_funcs = []

    #: Set an url_map property and set it as a Map object
    self.url_map = Map()
Copy the code

Here a Flask object is created and pointed to by the app variable, which is essentially an object that holds some configuration information, binds some view functions and has a URL map object (URL_map). We don’t yet know what this Map object is and what it does. From its name, it seems to Map urls to view functions. Routing import Map, Rule from Werkzeug. Routing import Map, Rule

The map class stores all the URL rules and some configuration parameters. Some of the configuration values are only stored on the Map instance since those affect all rules, others are just defaults and can be overridden for each rule. Note that you have to specify all arguments besides the rules as keyword arguments!

You can see that the object of this class stores all the URL rules and some configuration information. The Flask application (i.e. an instance of the Flask class) stores view functions and a URL mapping mechanism through the urL_map variable.

Application Startup Process

Flask class in app.py and werkZeug. serving, with special attention to run_simple BaseWSGIServer WSGIRequestHandler.

Line 6 of the Demo code is a restriction that says run the Flask program if the Python interpreter runs the file or package directly: In Python, if you execute a module or package directly, the interpreter sets the __name__ of the current module or package to __main_.

The run method in line 7 starts the Flask application:

def run(self, host=None, port=None, debug=None, **options):
    from werkzeug.serving import run_simple
    if host is None:
        host = '127.0.0.1'
    if port is None:
        server_name = self.config['SERVER_NAME']
        if server_name and ':' in server_name:
            port = int(server_name.rsplit(':'.1) [1])
        else:
            port = 5000
    if debug is not None:
        self.debug = bool(debug)
    options.setdefault('use_reloader', self.debug)
    options.setdefault('use_debugger', self.debug)
    try:
        run_simple(host, port, self, **options)
    finally:
        # reset the first request information if the development server
        # reset normally. This makes it possible to restart the server
        # without reloader and that stuff from an interactive shell.
        self._got_first_request = False
Copy the code

As you can see, this method is basically configuring the parameters. What actually starts the server is Werkzeug’s run_simple method, which starts the server BaseWSGIServer by default, Inherited from the Python standard library httpServer.tcpServer. Note that the Flask object passes in its own self when calling run_simple, which is correct, because the server must know whose __call__ method to call when it receives the request.

According to the httpServer. TCPServer model in the standard library, the server must have a class that acts as a Request handler for incoming requests, rather than an instance of HttpServer. TCPServer itself. Werkzeug provides the WSGIRequestHandler class to act as a request handler. This class, when called by BaseWSGIServer, executes this function:

def execute(app):
    application_iter = app(environ, start_response)
    try:
        for data in application_iter:
            write(data)
        if not headers_sent:
            write(b'')
    finally:
        if hasattr(application_iter, 'close'):
            application_iter.close()
        application_iter = None
Copy the code

The first line of the function, as WSGI requires, calls app and passes environ and start_Response. Let’s take a look at how in Flask calls to the server are echoed by WSGI requirements.

def __call__(self, environ, start_response):
    return self.wsgi_app(environ, start_response)

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)
Copy the code

You can see that Flask implements a __call__ method as required by WSGI, thus becoming a callable object. But instead of writing logic directly in __call__, it calls the wsGI_app method, which for middleware’s sake is beyond elaboration. In response(environ, start_Response), Response is an instance of the werkzueg.response class, which is also a callable object that generates the final traversable response body. And call start_response to form the response header.

Request processing

Flask’s code.

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)
Copy the code

The contents of the wsGI_app method are a high abstraction of the request processing.

Flask first establishes a RequestContext RequestContext object by calling request_context and pushes it into the _request_ctx_stack when receiving requests from the server. More on context and stack later, but what you need to know for now is that flask does this so that he doesn’t get confused when he handles multiple requests. Flask then calls the full_Dispatch_request method to distribute the request, starting the actual request processing process, which generates a response object and eventually returns it to the server by calling the response object. If there is an error, claim the corresponding error message. Eventually Flask pushes the request context off the stack, error or no error.

Full_dispatch_request is an entry for request distribution. Let’s look at its implementation:

def full_dispatch_request(self):
    self.try_trigger_before_first_request_functions()
    try:
        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)
Copy the code

First call the try_trigger_before_first_request_functions method to try to call the functions in the before_first_request list, If Flask’s _GOt_first_REQUEST property is False, the functions in before_first_REQUEST are executed, and after one execution _GOt_first_REQUEST is set to True and no longer executed.

The preprocess_request method is then called, which calls all the methods in the before_request_funcs list. If these before_request_funcs methods return something, the request will not really be distributed. For example, a before_request_funcs method that checks whether the user is logged in or not calls abort to return an error if the user is not logged in, and Flask does not distribute the request but reports the 401 error.

If the before_request_funcs function does not return, then the dispatch_request method is called for request distribution. This method first checks if the URL rule has the corresponding endpoint and value, if so, then calls the corresponding view function in view_functions (endpoint as the key) and passes the parameter value (**req.view_args). If not, it is handled by raise_ROUTing_exception. The return value of the view function or the error-handling view function is returned to the RV variable in the wsGI_app method.

def dispatch_request(self):
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        if getattr(rule, 'provide_automatic_options'.False) \
           and req.method == 'OPTIONS':
            return self.make_default_options_response()
        return self.view_functions[rule.endpoint](**req.view_args)

def finalize_request(self, rv, from_error_handler=False):
    response = self.make_response(rv)
    try:
        response = self.process_response(response)
        request_finished.send(self, response=response)
    except Exception:
        if not from_error_handler:
            raise
        self.logger.exception('Request finalizing failed with an '
                              'error while handling an error')
    return response

def make_response(self, rv):
    if isinstance(rv, self.response_class):
        return rv
    if isinstance(rv, basestring):
        return self.response_class(rv)
    if isinstance(rv, tuple):
        return self.response_class(*rv)
    return self.response_class.force_type(rv, request.environ)
Copy the code

Flask then generates a response based on the RV, and this make_response method checks to see if the RV is the required return value type, otherwise it generates the correct return type. For example, if the return value in Demo is a string, isinstance(RV, BaseString) judgment is satisfied and the response is generated from the string. Once this is done, Flask looks to see if there are any post-processing view functions that need to be executed (in the process_Response method) and finally returns a fully processed Response object.

View function registration

We’ve seen how Flask calls attempt functions in the request processing section, where we’ll focus on how Flask builds the data structures associated with request dispatch. We will focus on view_functions because the build process for other data structures such as before_request_funcs is much the same or even simpler. We’ll also take a closer look at what urL_map actually is, a question left over from the application creation section.

Line 4 of the Demo code registers a view function with the decorator route, which is a widely praised design in Flask. In the Flask class’s route method, you can see that it calls the add_url_rule method.

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

def add_url_rule(self, rule, endpoint, **options):
    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)
    options['endpoint'] = endpoint
    methods = options.pop('methods'.None)
    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)

    required_methods = set(getattr(view_func, 'required_methods', ()))

    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

    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 andold_func ! = view_func:raise AssertionError('View function mapping is overwriting an '
                                 'existing endpoint function: %s' % endpoint)
        self.view_functions[endpoint] = view_func
Copy the code

This method is responsible for registering view functions and mapping urls to view functions. First, it prepares an HTTP method supported by the view function (almost half of the code does this), then creates a rule object through URL_RUle_class and adds it to its URL_map. The rule object is a data structure that holds legal (Flask application supported) urls, methods, endpoints (in **options) and their relationships, and url_map is a collection of these objects. This method then adds the view function to view_functions, with the endpoint as its key and the default value being the function name.

Let’s take a closer look at rule, which is defined in werkzeug.routing.rule:

A Rule represents one URL pattern. There are some options for Rule that change the way it behaves and are passed to the Rule constructor. A Rule object represents a URL pattern and can change many of its behaviors by passing in parameters.

Rule’s __init__ method is:

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
Copy the code

Once a Rule is created, it is bound to a Map object via the Map add method, and flask.url_map is a Map object as we mentioned earlier.

def add(self, 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 bind method for a Rule adds a Map to the Rule, and then calls compile to generate a regular expression. Compile is a bit more complicated, so we won’t expand it.

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()
Copy the code

The rules bound to the URL_map are checked to find their corresponding view functions when Flask applications receive requests. This is implemented in the request context, as we saw earlier in the dispatch_request method — we get the rule from _request_CTx_stack.top. request and find the endpoint from that rule, Finally find the correct view function to handle the request. So, next we need to look at the implementation above and below the request, and see how Flask finds the rule from the URL_map.

def dispatch_request(self):
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    if getattr(rule, 'provide_automatic_options'.False) \
       and req.method == 'OPTIONS':
        return self.make_default_options_response()
    return self.view_functions[rule.endpoint](**req.view_args)
Copy the code

Request context

Ctx. RequestContext code.

How and when was the request context created? Flask’s wsGI_app, as we have seen before, creates the request context and pushes it when the server calls the application.

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
Copy the code

The request_context method is very simple; it simply creates an instance of the RequestContext class, defined in the flask.ctx file, which contains a list of information about the request, Most importantly, its own request attribute points to an instance of the Request class, which inherits from WerkZeug.Request. During RequestContext creation, It creates an instance of WerkZeug. Request based on incoming environ.

RequestContext’s push method is then called, which pushes itself to the top of the _request_CTx_stack.

The _request_CTx_stack is defined in the flask.global file, which is an instance of the LocalStack class implemented by Werkzeug. local. If you’re familiar with Python’s threading, You’ll see that thread isolation is implemented, which means that when the Python interpreter runs to the code associated with _request_CTx_STACK, it selects the correct instance based on the current process.

However, in analyzing the Flask source code, we did not find that Flask created threads after being called, so why do we need thread isolation? Look at the run function we mentioned at the beginning. It can actually pass a threaded argument. When we don’t pass this function, we start BasicWSGIServer, the server is single thread single process, Flask thread safety does not make sense, but when we pass this parameter, we start ThreadedWSGIServer, Flask’s thread-safety then makes sense, as it does in other multithreaded servers.

conclusion

A journey of request

Here, we intersect with this article by tracing the journey of a request to the server and back (by “becoming” a corresponding, of course).

  1. Flask registers all the view functions and URL mappings before the request is issued, and the server registers the Flask application on itself.
  2. The request arrives at the server and the server is readyenvironmake_responseFunction, and then calls the Flask application registered on him.
  3. The application implements WSGI requirementsapplication(environ, make_response)Methods. In Flask, this method is a by__call__The transit is calledwsgi_appMethods. It goes through firstenvironThe request context is created and pushed onto the stack so that Flask can access it as it processes the current request.
  4. Flask then starts processing the request, calling it in turnbefore_first_request_funcs before_request_funcs view_functionsAnd finally passfinalize_requestTo generate aresponseObject, in which the following group of functions will not be executed as long as the function returns a value,after_request_funcsforresponsePost-processing after generation.
  5. Flask calls thisresponseObject, and finally calledmake_responseFunction and returns a traversable response content.
  6. The server sends the response.

The Flask and werkzeug

In the analysis process, it is clear that Flask and Werkzeug are strongly coupled. In fact, WerkZeug is the only indispensable dependency of Flask. Some very detailed work is actually done by the WerkZeug library, and in this example, it does at least these things:

  1. encapsulationResponseRequestThe type is used by Flask, and in actual development, what we’re doing on request and response objects is actually callingwerkzeugMethods.
  2. Implement URL to view function mapping, and can pass the URL parameters to the view function. We’ve seen Flask’s URl_map property and seen how it binds view functions and error handlers, but the actual practice of mapping rules, and URL resolution in response, is all done by Werkzeug.
  3. Generated by the LocalProxy class_request_ctx_stackImplement thread protection for Flask.

Flask source code analysis for the moment here. If I have time, I’ll analyze the Flask template renderings, import Request, blueprints and some nice variables and functions, or dig into the Werkzeug library.

Refer to the reading

  1. Flask source code analysis series, you can read this series of articles to learn more details after reading the main line.
  2. Write a Web server together.

Article update record

  • September 10, 2017: Using version 0.12.0 for analysis, the structure and the order of writing have been readjustedwith many additions.