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

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

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

Лучший способ узнать о расширениях - посмотреть, как написаны другие расширения, которые вы используете, и обсудить с другими. Обсуждайте свои идеи дизайна с другими на наших форумах Discord Chat или GitHub Discussions.

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

Наименование

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

Общая рекомендация по упаковке Python заключается в том, что имя установки из индекса пакета и имя, используемое в операторах import, должны быть связаны. Имя импорта строчное, слова разделяются символами подчеркивания (_). Имя установки - строчное или заглавное, слова разделяются тире (-). Если она обертывает другую библиотеку, предпочитайте использовать тот же регистр, что и в имени этой библиотеки.

Вот несколько примеров имен установки и импорта:

  • Flask-Name импортируется как flask_name

  • flask-name-lower импортируется как flask_name_lower

  • Flask-ComboName импортируется как flask_comboname

  • Name-Flask импортируется как name_flask

Класс расширения и инициализация

Все расширения нуждаются в некоторой точке входа, которая инициализирует расширение с приложением. Наиболее распространенным шаблоном является создание класса, представляющего конфигурацию и поведение расширения, с методом init_app для применения экземпляра расширения к данному экземпляру приложения.

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

    def init_app(self, app):
        app.before_request(...)

Важно, чтобы приложение не хранилось на расширении, не делайте self.app = app. Единственный раз, когда расширение должно иметь прямой доступ к приложению, это во время init_app, в противном случае оно должно использовать current_app.

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

hello = HelloExtension()

def create_app():
    app = Flask(__name__)
    hello.init_app(app)
    return app

Выше было сказано, что экземпляр расширения hello существует независимо от приложения. Это означает, что другие модули в проекте пользователя могут делать from project import hello и использовать расширение в чертежах до того, как приложение будет существовать.

Диктант Flask.extensions можно использовать для хранения ссылки на расширение приложения или другого состояния, специфичного для приложения. Помните, что это единое пространство имен, поэтому используйте имя, уникальное для вашего расширения, например, имя расширения без префикса «flask».

Добавление поведения

Существует множество способов, которыми расширение может добавить поведение. Любые методы настройки, доступные для объекта Flask, могут быть использованы во время метода расширения init_app.

Общим шаблоном является использование before_request() для инициализации некоторых данных или соединения в начале каждого запроса, а затем teardown_request() для очистки в конце. Это может быть сохранено на g, о чем будет рассказано ниже.

Более ленивый подход заключается в предоставлении метода, который инициализирует и кэширует данные или соединение. Например, метод ext.get_db может создать соединение с базой данных при первом вызове, чтобы представление, которое не использует базу данных, не создавало соединение.

Помимо выполнения действий до и после каждого представления, ваше расширение может захотеть добавить некоторые специфические представления. В этом случае вы можете определить Blueprint, затем вызвать register_blueprint() во время init_app для добавления чертежа в приложение.

Техники конфигурирования

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

  • Конфигурация для каждого экземпляра приложения, через значения app.config. Это конфигурация, которая может разумно изменяться при каждом развертывании приложения. Обычный пример - URL-адрес внешнего ресурса, например, базы данных. Ключи конфигурации должны начинаться с имени расширения, чтобы они не мешали другим расширениям.

  • Конфигурация для каждого экземпляра расширения, через аргументы __init__. Эта конфигурация обычно влияет на то, как используется расширение, поэтому не имеет смысла изменять ее для каждого развертывания.

  • Конфигурация для каждого экземпляра расширения, через атрибуты экземпляра и методы декоратора. Возможно, более эргономичным будет присвоить значение ext.value или использовать декоратор @ext.register для регистрации функции после создания экземпляра расширения.

  • Глобальная настройка с помощью атрибутов класса. Изменение атрибута класса, например Ext.connection_class, позволяет настроить поведение по умолчанию без создания подкласса. Это может быть объединено с конфигурацией каждого расширения для переопределения значений по умолчанию.

  • Подклассификация и переопределение методов и атрибутов. Если сделать API самого расширения таким, которое можно переопределять, это даст очень мощный инструмент для расширенной настройки.

Сам объект Flask использует все эти техники.

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

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

Данные во время запроса

При написании приложения Flask объект g используется для хранения информации во время запроса. Например, tutorial хранит соединение с базой данных SQLite как g.db. Расширения также могут использовать его, но с некоторой осторожностью. Поскольку g является единым глобальным пространством имен, расширения должны использовать уникальные имена, которые не будут сталкиваться с пользовательскими данными. Например, используйте имя расширения как префикс или как пространство имен.

# an internal prefix with the extension name
g._hello_user_id = 2

# or an internal prefix as a namespace
from types import SimpleNamespace
g._hello = SimpleNamespace()
g._hello.user_id = 2

Данные в g длятся для контекста приложения. Контекст приложения активен, когда активен контекст запроса или когда выполняется команда CLI. Если вы храните что-то, что должно быть закрыто, используйте teardown_appcontext(), чтобы гарантировать, что оно будет закрыто, когда контекст приложения закончится. Если он должен быть действителен только во время запроса или не будет использоваться в CLI вне запроса, используйте teardown_request().

Виды и модели

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

Модель Post должна быть подклассом объекта Flask-SQLAlchemy db.Model, но это доступно только после создания экземпляра этого расширения, а не когда ваше расширение определяет свои представления. Как же код представления, определенный до существования модели, может получить доступ к модели?

Одним из методов может быть использование Представления на основе классов. Во время __init__ создайте модель, затем создайте представления, передав модель в метод as_view() класса представления.

class PostAPI(MethodView):
    def __init__(self, model):
        self.model = model

    def get(self, id):
        post = self.model.query.get(id)
        return jsonify(post.to_json())

class BlogExtension:
    def __init__(self, db):
        class Post(db.Model):
            id = db.Column(primary_key=True)
            title = db.Column(db.String, nullable=False)

        self.post_model = Post

    def init_app(self, app):
        api_view = PostAPI.as_view(model=self.post_model)

db = SQLAlchemy()
blog = BlogExtension(db)
db.init_app(app)
blog.init_app(app)

Другой техникой может быть использование атрибута для расширения, как, например, self.post_model сверху. Добавьте расширение к app.extensions в init_app, затем обратитесь к current_app.extensions["simple_blog"].post_model из представлений.

Возможно, вы также захотите предоставить базовые классы, чтобы пользователи могли создать свою собственную модель Post, соответствующую API, которую ожидает ваше расширение. Таким образом, они могут реализовать class Post(blog.BasePost), а затем установить ее как blog.post_model.

Как видите, все это может оказаться довольно сложным. К сожалению, идеального решения здесь нет, есть только различные стратегии и компромиссы в зависимости от ваших потребностей и того, насколько много настроек вы хотите предложить. К счастью, подобная зависимость от ресурсов не является обычной необходимостью для большинства расширений. Помните, если вам нужна помощь в разработке, спрашивайте на наших страницах Discord Chat или GitHub Discussions.

Вернуться на верх