• en
  • Language: ru
  • Documentation version: 1.1.x

Разработка расширений для Flask

Flask, будучи микрофреймворком, часто требует некоторых повторяющихся действий, чтобы заставить работать сторонние библиотеки. Многие такие расширения уже доступны на PyPI.

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

Анатомия расширения

Все расширения находятся в пакете под названием flask_something, где «something» - это имя библиотеки, которую вы хотите подключить. Например, если вы планируете добавить во Flask поддержку библиотеки с именем simplexml, вы назовете пакет своего расширения flask_simplexml.

Однако имя фактического расширения (человекочитаемое имя) должно быть примерно таким: «Flask-SimpleXML». Обязательно включите в это имя название «Flask» и проверьте капитализацию. Так пользователи смогут зарегистрировать зависимости от вашего расширения в своих файлах setup.py.

Но как выглядят сами расширения? Расширение должно обеспечивать работу сразу с несколькими экземплярами приложения Flask. Это требование, потому что многие люди будут использовать шаблоны, подобные шаблону Заводы по производству приложений, для создания своего приложения по мере необходимости, чтобы помочь unittests и поддержать несколько конфигураций. Поэтому очень важно, чтобы ваше приложение поддерживало такое поведение.

Самое главное, расширение должно поставляться с файлом setup.py и быть зарегистрированным на PyPI. Также должна работать ссылка на проверку разработки, чтобы люди могли легко установить версию разработки в свой virtualenv без необходимости скачивать библиотеку вручную.

Расширения Flask должны быть лицензированы по BSD, MIT или более либеральной лицензии, чтобы быть внесенными в реестр расширений Flask. Помните, что реестр расширений Flask - это модерируемое место, и библиотеки будут проверяться заранее, если они ведут себя так, как требуется.

«Hello Flaskext!»

Итак, давайте приступим к созданию такого расширения Flask. Расширение, которое мы хотим создать, будет обеспечивать базовую поддержку SQLite3.

Сначала мы создадим следующую структуру папок:

flask-sqlite3/
    flask_sqlite3.py
    LICENSE
    README

Вот содержимое самых важных файлов:

setup.py

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

"""
Flask-SQLite3
-------------

This is the description for that library
"""
from setuptools import setup


setup(
    name='Flask-SQLite3',
    version='1.0',
    url='http://example.com/flask-sqlite3/',
    license='BSD',
    author='Your Name',
    author_email='your-email@example.com',
    description='Very short description',
    long_description=__doc__,
    py_modules=['flask_sqlite3'],
    # if you would be using a package instead use packages instead
    # of py_modules:
    # packages=['flask_sqlite3'],
    zip_safe=False,
    include_package_data=True,
    platforms='any',
    install_requires=[
        'Flask'
    ],
    classifiers=[
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
        'Topic :: Software Development :: Libraries :: Python Modules'
    ]
)

Это много кода, но вы можете просто скопировать/вставить его из существующих расширений и адаптировать.

flask_sqlite3.py

Именно здесь находится код вашего расширения. Но как именно должно выглядеть такое расширение? Какова наилучшая практика? Продолжайте читать, чтобы понять это.

Инициализация расширений

Многим расширениям потребуется какой-то шаг инициализации. Например, рассмотрим приложение, которое в данный момент подключается к SQLite, как это предлагается в документации (Использование SQLite 3 с Flask). Как же расширение узнает имя объекта приложения?

Все очень просто: вы передаете его ему.

Существует два рекомендуемых способа инициализации расширения:

функции инициализации:

Если ваше расширение называется helloworld, у вас может быть функция init_helloworld(app[, extra_args]), которая инициализирует расширение для этого приложения. Она может подключать обработчики до/после и т.д.

классы:

Классы работают в основном как функции инициализации, но позже могут быть использованы для дальнейшего изменения поведения. Для примера посмотрите, как работает OAuth extension: есть объект OAuth, который предоставляет некоторые вспомогательные функции, такие как OAuth.remote_app для создания ссылки на удаленное приложение, использующее OAuth.

Что использовать, зависит от того, что вы задумали. Для расширения SQLite 3 мы будем использовать подход, основанный на классах, поскольку он предоставит пользователям объект, обрабатывающий открытие и закрытие соединений с базой данных.

При разработке классов важно сделать их легко повторно используемыми на уровне модуля. Это означает, что сам объект ни при каких обстоятельствах не должен хранить состояние, специфичное для конкретного приложения, и должен быть доступен для совместного использования различными приложениями.

Код расширения

Вот содержимое flask_sqlite3.py для копирования/вставки:

import sqlite3
from flask import current_app, _app_ctx_stack


class SQLite3(object):
    def __init__(self, app=None):
        self.app = app
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        app.config.setdefault('SQLITE3_DATABASE', ':memory:')
        app.teardown_appcontext(self.teardown)

    def connect(self):
        return sqlite3.connect(current_app.config['SQLITE3_DATABASE'])

    def teardown(self, exception):
        ctx = _app_ctx_stack.top
        if hasattr(ctx, 'sqlite3_db'):
            ctx.sqlite3_db.close()

    @property
    def connection(self):
        ctx = _app_ctx_stack.top
        if ctx is not None:
            if not hasattr(ctx, 'sqlite3_db'):
                ctx.sqlite3_db = self.connect()
            return ctx.sqlite3_db

Вот что делают эти строки кода:

  1. Метод __init__ принимает необязательный объект app и, если он предоставлен, вызывает init_app.

  2. Метод init_app существует для того, чтобы объект SQLite3 мог быть инстанцирован без необходимости создания объекта приложения. Этот метод поддерживает модель фабрики для создания приложений. Метод init_app задает конфигурацию базы данных, по умолчанию используется база данных в памяти, если конфигурация не задана. Кроме того, метод init_app присоединяет обработчик teardown.

  3. Далее мы определяем метод connect, который открывает соединение с базой данных.

  4. Наконец, мы добавляем свойство connection, которое при первом обращении открывает соединение с базой данных и сохраняет его в контексте. Это также рекомендуемый способ работы с ресурсами: лениво извлекать ресурсы при первом использовании.

    Обратите внимание, что мы присоединяем наше соединение с базой данных к верхнему контексту приложения через _app_ctx_stack.top. Расширения должны использовать верхний контекст для хранения собственной информации с достаточно сложным именем.

Почему же мы решили использовать подход, основанный на классах? Потому что использование нашего расширения выглядит примерно так:

from flask import Flask
from flask_sqlite3 import SQLite3

app = Flask(__name__)
app.config.from_pyfile('the-config.cfg')
db = SQLite3(app)

Затем вы можете использовать базу данных из представлений следующим образом:

@app.route('/')
def show_all():
    cur = db.connection.cursor()
    cur.execute(...)

Аналогичным образом, если вы находитесь вне запроса, вы можете использовать базу данных, передав контекст приложения:

with app.app_context():
    cur = db.connection.cursor()
    cur.execute(...)

В конце блока with будет автоматически выполнена обработка разрыва.

Кроме того, метод init_app используется для поддержки паттерна фабрики для создания приложений:

db = SQLite3()
# Then later on.
app = create_app('the-config.cfg')
db.init_app(app)

Помните, что поддержка этого фабричного паттерна для создания приложений требуется для одобренных расширений flask (описанных ниже).

Примечание по init_app

Как вы заметили, init_app не присваивает app к self. Это сделано намеренно! Расширения Flask, основанные на классах, должны хранить приложение на объекте только в том случае, если приложение было передано конструктору. Это говорит расширению: Я не заинтересован в использовании нескольких приложений.

Когда расширению нужно найти текущее приложение, а у него нет ссылки на него, оно должно либо использовать локальный контекст current_app, либо изменить API таким образом, чтобы можно было передавать приложение явно.

Использование _app_ctx_stack

В приведенном выше примере перед каждым запросом переменной sqlite3_db присваивается значение _app_ctx_stack.top. В функции представления эта переменная доступна с помощью свойства connection SQLite3. Во время завершения запроса соединение sqlite3_db закрывается. Благодаря использованию этого паттерна, одно и то же соединение с базой данных sqlite3 доступно всему, что в нем нуждается, в течение всего времени выполнения запроса.

Учитесь у других

Эта документация затрагивает лишь самый минимум для разработки расширений. Если вы хотите узнать больше, вам стоит ознакомиться с существующими расширениями на PyPI. Если вы чувствуете себя потерянным, есть еще mailinglist и Discord server, где можно почерпнуть несколько идей для красивых API. Особенно если вы делаете что-то, чего никто до вас не делал, может быть очень хорошей идеей получить еще несколько предложений. Это не только создает полезную обратную связь о том, что люди могут хотеть от расширения, но и позволяет избежать изолированной работы нескольких разработчиков над практически одной и той же проблемой.

Помните: хороший дизайн API - это сложно, поэтому представьте свой проект в списке рассылки, и пусть другие разработчики протянут вам руку помощи в проектировании API.

Лучшие расширения Flask - это расширения, которые разделяют общие идиомы для API. А это может сработать только в том случае, если сотрудничество происходит на ранних этапах.

Утвержденные пристройки

Ранее во Flask существовала концепция одобренных расширений. Они сопровождались определенной проверкой на поддержку и совместимость. Хотя этот список со временем стало слишком сложно поддерживать, рекомендации по-прежнему актуальны для всех расширений, поддерживаемых и разрабатываемых сегодня, поскольку они помогают экосистеме Flask оставаться последовательной и совместимой.

  1. Одобренное расширение Flask требует сопровождающего. Если автор расширения захочет выйти из проекта, проект должен найти нового сопровождающего и передать ему доступ к репозиторию, документации, PyPI и любым другим сервисам. Если мейнтейнер недоступен, предоставьте доступ основной команде Pallets.

  2. Схема именования - Flask-ExtensionName или ExtensionName-Flask. Он должен предоставлять ровно один пакет или модуль с именем flask_extension_name.

  3. Расширение должно быть лицензировано BSD или MIT. Оно должно быть с открытым исходным кодом и общедоступным.

  4. API расширения должен обладать следующими характеристиками:

    • Он должен поддерживать несколько приложений, запущенных в одном процессе Python. Используйте current_app вместо self.app, храните конфигурацию и состояние для каждого экземпляра приложения.

    • Должна быть возможность использовать паттерн фабрики для создания приложений. Используйте паттерн ext.init_app().

  5. Из клона репозитория расширение с его зависимостями должно устанавливаться с помощью pip install -e ..

  6. Он должен поставлять набор тестов, который может быть вызван с помощью tox -e py или pytest. Если не используется tox, зависимости тестов должны быть указаны в файле requirements.txt. Тесты должны быть частью дистрибутива sdist.

  7. Документация должна использовать тему flask из Official Pallets Themes. Ссылка на документацию или сайт проекта должна быть в метаданных PyPI или в readme.

  8. Для максимальной совместимости расширение должно поддерживать те же версии Python, которые поддерживает Flask. По состоянию на 2020 год рекомендуется 3.6+. Используйте python_requires=">= 3.6" в setup.py для указания поддерживаемых версий.