анализ исходного кода фляги

Flask

В этой статье кратко анализируется исходный код Flask с акцентом на WSGI, структуру данных объекта Flask, процесс запуска приложения Flask, процесс обработки запроса, функцию просмотра, сопоставление URL-адресов, контекст приложения и контекст запроса. Эти темы не будут освещены во всех подробностях, поэтому, пожалуйста, изучите их самостоятельно, так как вам нужно прочитать исходный код. Чтобы прочитать эту статью, вам нужно быть знакомым с Flask, например, вы написали небольшой проект на Flask, и иметь определенные навыки чтения кода, и иметь базовое представление о функциях веб-фреймворка.

Эта статья будет время от времени обновляться, дата последнего обновления: 10 сентября 2017 г.

Это официальный демо-код Flask:

from flask import Flask
app = Flask(__name__)

@app.route(‘/‘)
def index():
    return ‘Hello, world!’

if __name__ == ‘__main__’:
    app.run()

Эта статья начинается с этого простого кода и кратко знакомит с WSGI, структурой данных объекта Flask, процессом запуска приложения Flask, процессом обработки запросов, функциями просмотра, сопоставлением URL-адресов, классами запросов и ответов (контекст приложения и контекст запроса). веб-фреймворк.

WSGI

После того, как запрос, инициированный пользователем, достигает сервера, он будет получен HTTP-сервером, а затем передан в веб-приложение для обработки бизнеса, поэтому между сервером HTTP и веб-приложения требуется интерфейс между сервером и веб-приложением. В мире Python Web Development, официальный официальный PythonНазначенсоздал этот интерфейс и назвал его WSGI, как указано в PEP333. Любая комбинация серверов и фреймворков может быть реализована, если и серверы, и фреймворки придерживаются этого соглашения. Согласно этому регламенту, фреймворк, ориентированный на WSGI, должен реализовать этот метод:

def application(environ, start_response)

В процессе работы HTTP-сервер вызовет указанный выше метод и передаст информацию запроса, которая называетсяenvironсловарь иstart_responseфункция, применяемая изenvironПолучите информацию о запросах в и назовете ее после обработки бизнесаstart_responseУстановите заголовок ответа и верните тело ответа (должно быть проходимым объектом, таким как список, словарь) на HTTP-сервер, а HTTP-сервер вернет ответ пользователю.

Итак, Flask, как веб-фреймворк для разработки веб-приложений, отвечает за решение следующих задач:

  1. Как приложение, которое может быть вызвано сервером HTTP, оно должно иметь__call__метод
  2. Через информацию о входящем запросе (URL, метод HTTP и т. д.) найдите правильную логику бизнес-процессинга, то есть правильную функцию просмотра
  3. Обработка бизнес-логики, которая может включать проверки форм, CRUD базы данных и т. д. (это не будет рассматриваться в этой статье).
  4. вернуть правильный ответ
  5. При одновременной обработке нескольких запросов также необходимо защищать эти запросы и знать, какой ответ следует использовать для соответствия какому запросу, то есть защиту потока.

Давайте посмотрим, как Flask решает эти проблемы.

Справочное чтение:Напишем веб-сервер вместеЭта серия статей даст вам базовое понимание того, как веб-серверы и рамки работают вместе через WSGI.

Создание приложений

Чтение исходного кода:app.pyсерединаFlaskкод класса.

Вторая строка демонстрационного кода создает экземпляр класса Flask, передавая имя текущего модуля. Давайте сначала посмотрим, что такое приложение Flask и какова его структура данных.

Flaskтакой класс:

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).

Объект колбы на самом деле является приложением WSGI. Он получает имя модуля или пакета в качестве параметра. После того, как он создан, все функции просмотра, правила URL, настройки шаблона и т. Д. Зарегистрированы выше. Название модуля или пакета состоит в том, чтобы найти некоторые ресурсы.

Класс Flask имеет некоторые свойства:

  • request_class = RequestУстановите тип запроса
  • response_class = ResponseУстановите тип ответа

Оба типа являются производными от его зависимой библиотекиwerkzeugИ сделал простое расширение.

Объект фляги__init__Методы, как показано ниже:

def __init__(self, package_name):
    #: Flask 对象有这样一个字典来保存所有的视图函数
    self.view_functions = {}

    #: 这个字典用来保存所有的错误处理视图函数
    #: 字典的 key 是错误类型码
    self.error_handlers = {}

    #: 这个列表用来保存在请求被分派之前应当执行的函数
    self.before_request_funcs = []

    #: 在接收到第一个请求的时候应当执行的函数
    self.before_first_request_funcs = []

    #: 这个列表中的函数在请求完成之后被调用,响应对象会被传给这些函数
    self.after_request_funcs = []

    #: 这里设置了一个 url_map 属性,并把它设置为一个 Map 对象
    self.url_map = Map()

Колба здесь, чтобы создать полные и переменные объектыappОн указывает на то, что на самом деле это объект сопоставления URL-адресов, который сохраняет некоторую информацию о конфигурации, связывает некоторые функции представления и имеет объект сопоставления URL-адресов (url_map)Объект. Но мы до сих пор не знаем, что это за объект Map и что он делает.Из названия кажется, что его роль заключается в сопоставлении URL-адресов с функциями просмотра. Строка 21 исходного кода имеетfrom werkzeug.routing import Map, Rule, тогда посмотримwerkzeugОпределение карты в этой библиотеке:

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!

Вы можете видеть, что объекты этого класса хранят все правила URL и некоторую информацию о конфигурации. так какwerkzeugМеханизм маппинга более сложный.Подробнее о механизме маппинга мы узнаем позже.Теперь просто вспомним приложение Flask (то есть приложение Flask).Flaskэкземпляр класса) сохранить функцию просмотра и передатьurl_mapВ этой переменной хранится механизм сопоставления URL-адресов.

Процесс запуска приложения

Чтение исходного кода:app.pyсерединаFlaskкод класса иwerkzeug.servingкод, обратите особое внимание наrun_simple BaseWSGIServer WSGIRequestHandler.

Строка 6 демонстрационного кода представляет собой ограничение, указывающее, что если интерпретатор Python напрямую запускает файл или пакет, запускается программа Flask: в Python, если модуль или пакет выполняется напрямую, интерпретатор будет использовать текущий модуль или пакет. , пакет__name__установлен в__main_.

в строке 7runметод запускает приложение Flask:

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

Такой подход можно увидеть по сути в параметрах конфигурации, собственно запуск сервераwerkzeugизrun_simpleметод, который запускает сервер по умолчаниюBaseWSGIServer, унаследованная от стандартной библиотеки PythonHTTPServer.TCPServer.注意在调用run_simpleselfОн передается как параметр, и это правильно, потому что, когда сервер получает запрос, он должен знать, кому звонить.__call__метод.

Согласно стандартной библиотекеHTTPServer.TCPServerрежиме сервер должен иметь класс, который действует как обработчик запросов для обработки входящих запросов, а неHTTPServer.TCPServerсам экземпляр для обработки,werkzeugпри условииWSGIRequestHandlerкласс в качестве обработчика запроса, этот класс используетсяBaseWSGIServerЭта функция выполняется при вызове:

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

Первая строка функции соответствует требованиям WSGI, и для вызова приложенияenvironиstart_responseвходящий. Давайте посмотрим, как flask отвечает на вызовы сервера в соответствии с требованиями WSGI.

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)

Вы можете видеть, что Flask реализован в соответствии с требованиями WSGI.__call__Метод, следовательно, становится объектом вызова. Но это не напрямую__call__Логика логична, но называетсяwsgi_appМетод, это для рассмотрения промежуточного программного обеспечения, поэтому я не буду говорить об этом. возвращено этим методомresponse(environ, start_response)середина,responseдаwerkzueg.responseЭкземпляр класса, который также является вызываемым объектом, который отвечает за генерацию конечного проходимого тела ответа и вызовstart_responseСформируйте заголовок ответа.

обработка запроса

Чтение исходного кода:app.Flaskкод.

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)

wsgi_appСодержимое метода представляет собой высокую абстракцию процесса обработки запросов.

Во-первых, при получении запроса от сервера Flask вызываетrequest_contextфункция создаетRequestContextзапросить объект контекста и вставить его в_request_ctx_stackкуча. Контекст и стек будут обсуждаться позже, но сейчас вам нужно знать, что эти операции нужны для того, чтобы flask не запутался при обработке нескольких запросов. После этого Flask позвонитfull_dispatch_requestМетод распространяется на этот запрос, запуская фактический процесс обработки запроса, который генерирует объект ответа и в конечном итоге вызываетresponseобъект, чтобы вернуться на сервер. Если есть ошибка, утверждайте соответствующее сообщение об ошибке. Независимо от ошибок, в конечном итоге колба будет толкать контекст запроса от стека.

full_dispatch_requestявляется точкой входа распределения запросов, давайте посмотрим на ее реализацию:

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)

первый звонокtry_trigger_before_first_request_functionsметод, чтобы попытаться позвонитьbefore_first_requestфункция в списке, еслиFlaskиз_got_first_requestсобственностьFalse,before_first_requestФункция будет выполнена один раз после выполнения,_got_first_requestбудет установлен наTrueТаким образом, эти функции больше не выполняются.

тогда позвониpreprocess_requestметод, этот метод вызываетbefore_request_funcsвсе методы в списке, если ониbefore_request_funcsМетод что-то возвращает, то запрос на самом деле не отправляется. Например,before_request_funcsМетод используется для определения того, вошел ли пользователь в систему, если пользователь не вошел в систему, этот метод вызоветabortТаким образом, метод возвращает ошибку, и Flask не будет отправлять запрос, а напрямую сообщит об ошибке 401.

еслиbefore_request_funcsФункция in не возвращается, затем снова вызовитеdispatch_requestспособ отправки запроса. Этот метод сначала проверит, есть ли соответствующий URL в правилахendpointиvalueзначение, если есть, то звонитеview_functionsСоответствующая функция представления в (endpointв качестве значения ключа) и передать значение параметра (**req.view_args), если нет, тоraise_routing_exceptionдля обработки. Возвращаемое значение функции представления или возвращаемое значение функции представления обработки ошибок будет возвращено вwsgi_appв методеrvПеременная.

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)

Тогда колба будетrvчтобы получить ответ, этоmake_responseМетод проверяет, является ли rv требуемым типом возвращаемого значения, в противном случае генерирует правильный тип возвращаемого значения. Например, если возвращаемое значение в Demo является строкой, оно удовлетворитisinstance(rv, basestring)Оцените и сгенерируйте ответ из строки. После завершения этого шага Flask проверяет, есть ли функции просмотра постобработки для выполнения (вprocess_responseметод) и в конечном итоге возвращает полностью обработанныйresponseобъект.

Просмотр регистрации функций

В разделе об обработке запросов мы видели, как Flask вызывает функцию попытки, В этом разделе мы сосредоточимся на том, как Flask создает структуры данных, связанные с диспетчеризацией запросов. Мы сосредоточимся в основном наview_functions, потому что другие структуры данных, такие какbefore_request_funcsПроцесс сборки почти такой же или даже проще. Также подробнее рассмотрим вопросы, оставшиеся в разделе о создании приложения, а именноurl_mapчто именно это такое.

Строка 4 демонстрационного кода использует декораторrouteЗарегистрируйте функцию просмотра, которая широко известна колбой в дизайне. В классе колбаrouteметод, вы можете видеть, что он вызываетadd_url_ruleметод.

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

Этот метод отвечает за регистрацию функций просмотра и реализацию сопоставления URL-адресов с функциями просмотра. Во-первых, ему нужно подготовить метод HTTP, поддерживаемый функцией просмотра (в основном это делает более половины кода), а затем передатьurl_rule_classСоздаватьruleобъект и добавить этот объект к своемуurl_mapвнутри. Ответ на наш оставшийся вопрос здесь:ruleЦель состоит в том, чтобы сохранить законный (Flask, поддерживаемый приложением) URL-адрес, метод,endpoint(существует**options) и соответствующие им структуры данных, аurl_mapэто коллекция, которая содержит эти объекты. Затем этот метод добавляет функцию просмотра вview_functionsсреди,endpointВ качестве ключа его значением по умолчанию является имя функции.

Давайте посмотрим поближеruleЭто определено в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Конструктор. Объект Rule представляет собой режим URL-адреса, который может изменять многие из своих свойств с помощью входящих параметров.

Правила__init__Метод:

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

После создания правила перейдитеMapизaddметод привязан кMapобъект, мы сказали ранееflask.url_mapтолько одинMapобъект.

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

иRuleизbindСодержание метода состоит в том, чтобы добавитьRuleсоответствующийMap, а затем позвонитеcompileСпособ генерации регулярного выражения,compileМетод более сложный и не будет расширяться.

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()

Когда приложение Flask получает запрос, они привязываются кurl_mapВверхRuleпросматриваются, чтобы найти соответствующие им функции просмотра. Это реализовано в контексте запроса, который мы ранее использовали вdispatch_requestметод - мы из_request_ctx_stack.top.requestполучитьruleи от этогоruleоказатьсяendpointи, наконец, найти правильную функцию представления для обработки запроса. Итак, далее нам нужно посмотреть на конкретную реализацию запроса вверх и вниз и посмотреть, как Flask получает отurl_mapнашел это вruleиз.

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)

контекст запроса

Чтение исходного кода:ctx.RequestContextкод.

Как и когда создается контекст запроса? Мы также видели, что когда сервер вызывает приложение, Flask'swsgi_appЕсть такой оператор, который должен создать контекст запроса и поместить его в стек.

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

request_contextМетод очень простой, просто создайтеRequestContextЭкземпляр класса, определенного вflask.ctxфайл, он содержит ряд информации о запросе,самое главное этосвой собственныйrequestатрибут указывает наRequestЭкземпляр класса, этот класс наследует отwerkzeug.Request,существуетRequestContextпроцесс создания, он будетenvironСоздаватьwerkzeug.Requestпример.

тогдаRequestContextизpushМетод вызывается, этот метод подтолкнет себя к_request_ctx_stackВерх стека.

_request_ctx_stackопределяется вflask.globalфайл, этоLocalStackЭкземпляр классаwerkzeug.localЧто достигается, если вы знакомы с потоковой передачей Python, вы обнаружите, что здесь реализована изоляция потоков, то есть когда интерпретатор Python запускается в_request_ctx_stackКогда дело доходит до связанного кода, интерпретатор выберет правильный экземпляр на основе текущего процесса.

Однако за весь процесс анализа исходного кода Flask мы не обнаружили, что Flask создавал поток после вызова, так зачем же делать изоляцию потоков? посмотрите, что мы упоминали в началеrunФункция, на самом деле, это может пройтиthreadedпараметр. Когда эта функция не передается, мы начинаемBasicWSGIServer, этот сервер однопоточный и однопроцессный, и потокобезопасность Flask, естественно, бессмысленна, но когда мы передаем этот параметр, мы запускаем следующее:ThreadedWSGIServer, то потокобезопасность Flask имеет смысл, как и на других многопоточных серверах.

Суммировать

запрошенное путешествие

Здесь мы проходим по этой статье, отслеживая путь запроса к серверу и обратно (конечно, «становясь» ответом).

  1. Прежде чем выдавать запрос, Flask зарегистрировал хороший вид на все функции и сопоставления URL, сервер на зарегистрированном приложении Flask.
  2. Запрос поступает на сервер, сервер готовenvironиmake_responseфункцию, а затем вызовите приложение Flask, зарегистрированное на себя.
  3. Приложение реализует требования WSGIapplication(environ, make_response)метод. Во Flask этот метод называется__call__Транзит называетсяwsgi_appМетоды.它首先通过environКонтекст запроса создается и помещается в стек, чтобы flask мог получить доступ к контексту запроса при обработке текущего запроса.
  4. Затем Flask начинает обрабатывать запрос, вызывая по очередиbefore_first_request_funcs before_request_funcs view_functionsфункция в и, наконец, передатьfinalize_requestгенерироватьresponseОбъект, пока есть возвращаемое значение функции, следующая группа функций не будет выполняться снова,after_request_funcsпровестиresponseПосле обработки после поколения.
  5. Фласк называет этоresponseобъект, который в итоге вызываетmake_responseфункция и возвращает проходимое содержимое ответа.
  6. Сервер отправляет ответ.

Фляга и Werkzeug

В ходе анализа стало очевидно, что Flask иwerkzeugНа самом деле это сильно связано,werkzeugЭто единственная необходимая зависимость от Flask.werkzeugБиблиотека, в случае с этой статьей, делает как минимум следующие вещи:

  1. упаковкаResponseиRequestТип используется Flask.В реальной разработке наши операции над объектами запроса и ответа фактически называютсяwerkzeugМетоды.
  2. Реализуйте сопоставление URL-адреса с функцией просмотра и можете передавать параметры в URL-адресе функции просмотра. Мы видели атрибуты Flask URL_MAP и видели, как они связывают функции просмотра и функции обработки ошибок, но практика конкретных правил сопоставления и анализ URL-адресов во время процесса ответа выполняются Werkzeug.
  3. Генерируется классом LocalProxy_request_ctx_stackРеализовать защиту потока для Flask.

Для анализа исходного кода Flask давайте временно остановимся здесь. Когда у меня будет время, я проанализирую отрисовку шаблона во Flask,import request, чертежи и некоторые полезные переменные и функции, или углубленный анализwerkzeugбиблиотека.

эталонное чтение

  1. Серия статей об анализе исходного кода Flask, вы можете прочитать эту серию статей для получения более подробной информации после прочтения этой статьи, чтобы понять основную линию.
  2. Напишем веб-сервер вместе.

Запись обновления статьи

  • 10 сентября 2017 г .: Проанализировано с версией 0.12.0, реструктурировано и упорядочено, добавлено много контента.