Introducing Flask Unchained

Flask Unchained is a Flask extension, a pluggable application factory, and a set of mostly optional “bundles” that together create a modern, fully integrated, and highly customizable web framework for Flask and its extension ecosystem. Flask Unchained aims to stay true to the spirit and API of Flask, while making it significantly easier to quickly build large web apps and APIs with Flask and SQLAlchemy.

Hello World

Install Flask Unchained

Requires Python 3.6+

pip install "flask-unchained[dev]"

A Basic App

Almost as simple as it gets:

# project-root/app.py
from flask_unchained import AppBundle, Controller, Service, injectable, route
from flask_unchained.pytest import HtmlTestClient

BUNDLES = ['flask_unchained.bundles.controller']

class App(AppBundle):
    pass

class HelloService(Service):
    def hello_world(self, name: str) -> str:
        return f'Hello World from {name}!'

class SiteController(Controller):
    hello_service: HelloService = injectable

    @route('/')
    def index(self) -> str:
        return self.hello_service.hello_world('Flask Unchained')


class TestSiteController:
    def test_index(self, client: HtmlTestClient):
        r = client.get('site_controller.index')
        assert r.status_code == 200
        assert r.html == 'Hello World from Flask Unchained!'

If you’ve used Flask before, the familiar call to app = Flask(__name__) isn’t missing… That’s a tiny working app right there. Under the easily accessible hood, everything is still just good old Flask. No plumbing, no boilerplate, everything just works!

You can run it like so:

cd project-root
export UNCHAINED_CONFIG="app"
pytest
flask run

Included bundles & integrated extensions

Alongside integrating Flask extensions for use by other Flask Unchained apps and bundles, bundles also:

  • utilize simple, consistent, and configurable patterns for writing and sharing Flask code that allows customizing everything from anywhere

  • can be standalone/redistributable Python packages (that share the same patterns for customization, ad infinitum)

  • can be full-blown apps (like say a blog or web store) that your app can integrate, extend, and/or customize

  • each bundle with views is itself a Flask Blueprint (automatically)

New in v0.8: asyncio support [experimental]

Thanks to Quart, Flask Unchained now has preliminary support for building asynchronous web apps with asyncio:

pip install "flask-unchained[dev]" quart
# project-root/app.py
from flask_unchained import AppBundle, Controller, route
from quart import websocket

class App(AppBundle):
    pass

class SiteController(Controller):
    @route('/')
    async def index(self):
        return await self.render('index')

    @route('/ws', is_websocket=True)
    async def ws(self):
        while True:
            data = await websocket.receive()
            await websocket.send(f'echo {data}')
<!-- project-root/templates/site/index.html -->
<!doctype html>
<html>
<head>
  <title>Websocket example</title>
</head>
<body>
  <input type="text" id="message">
  <button type="submit">Send</button>
  <ul></ul>
  <script type="text/javascript">
    var ws = new WebSocket('ws://' + document.domain + ':' + location.port + '/ws');
    ws.onmessage = function (event) {
      var messages_dom = document.getElementsByTagName('ul')[0];
      var message_dom = document.createElement('li');
      var content_dom = document.createTextNode('Received: ' + event.data);
      message_dom.appendChild(content_dom);
      messages_dom.appendChild(message_dom);
    };

    var button = document.getElementsByTagName('button')[0];
    button.onclick = function () {
      var content = document.getElementsByTagName('input')[0].value;
      ws.send(content);
    };
  </script>
</body>
</html>
cd project-root
UNCHAINED_CONFIG="app" flask run

Please note that some included bundles won’t work, because they integrate Flask extensions that don’t work with asyncio. Contributions welcome ;)

Disclaimer: this is alpha/beta quality software

The core is solid, but especially at the edges, there will likely be bugs - and possibly some breaking API changes too. Please file issues on GitHub if you have any questions, encounter any problems, and/or have any feedback!

Thanks and acknowledgements

The architecture of how Flask Unchained and its bundles work is inspired by the Symfony Framework, which is awesome, aside from the fact that it isn’t Python ;)

The Unchained Extension

The “orchestrator” that ties everything together. It handles dependency injection and enables access to much of the public API of flask.Flask and flask.Blueprint:

# project-root/app.py
from flask_unchained import unchained, injectable

@unchained.inject()
def print_hello(name: str, hello_service: HelloService = injectable):
    print(hello_service.hello_world(name))

@unchained.before_first_request
def runs_once_at_startup():
    print_hello("App")

@unchained.app.after_request
def runs_after_each_request_to_an_app_bundle_view(response):
    print_hello("Response")
    return response

The Unchained Extension also plays a role in the app factory:

The App Factory

The app factory discovers all the code from your app and its bundles, and then with it automatically initializes, configures, and “boots up” the Flask app instance for you. I know that sounds like magic, but it’s actually quite easy to understand, and every step it takes can be customized by you if necessary. In barely-pseudo-code, the app factory looks like this:

from flask import Flask
from flask_unchained import DEV, PROD, STAGING, TEST

class AppFactory:
    APP_CLASS = Flask

    def create_app(self, env: Union[DEV, PROD, STAGING, TEST]) -> Flask:
        # load the Unchained Config and configured bundles
        unchained_config = self.load_unchained_config(env)
        app_bundle, bundles = self.load_bundles(unchained_config.BUNDLES)

        # instantiate the Flask app instance
        app = self.APP_CLASS(app_bundle.name, **kwargs_from_unchained_config)

        # let bundles configure the app pre-initialization
        for bundle in bundles:
            bundle.before_init_app(app)

        # discover code from bundles and boot the app using hooks
        unchained.init_app(app, bundles)
            # the Unchained extension runs hooks in their correct order:
            RegisterExtensionsHook.run_hook(app, bundles)
            ConfigureAppHook.run_hook(app, bundles)
            InitExtensionsHook.run_hook(app, bundles)
            RegisterServicesHook.run_hook(app, bundles)
            RegisterCommandsHook.run_hook(app, bundles)
            RegisterRoutesHook.run_hook(app, bundles)
            RegisterBundleBlueprintsHook.run_hook(app, bundles)
            # (there may be more depending on which bundles you enable)

        # let bundles configure the app post-initialization
        for bundle in bundles:
            bundle.after_init_app(app)

        # return the app instance ready to rock'n'roll
        return app

The flask and pytest CLI commands automatically use the app factory for you, while in production you have to call it yourself:

# project-root/wsgi.py
from flask_unchained import AppFactory, PROD

app = AppFactory().create_app(env=PROD)

For a deeper look check out How Flask Unchained Works.

Going Big (Project Layout)

When you want to expand beyond a single file, Flask Unchained defines a configurable folder structure for you so that everything just works. A common structure might look like this:

/home/user/dev/project-root
├── unchained_config.py # the Flask Unchained config
├── app                 # your App Bundle package
│   ├── admins          # Flask-Admin model admins
│   ├── commands        # Click CLI groups/commands
│   ├── extensions      # Flask extensions
│   ├── models          # SQLAlchemy models
│   ├── fixtures        # SQLAlchemy model fixtures (for seeding the dev db)
│   ├── serializers     # Marshmallow serializers (aka schemas)
│   ├── services        # dependency-injectable Services
│   ├── tasks           # Celery tasks
│   ├── templates       # Jinja2 templates
│   ├── views           # Controllers, Resources and ModelResources
│   ├── __init__.py
│   ├── config.py       # your app config
│   └── routes.py       # declarative routes
├── bundles             # custom bundles and/or bundle extensions/overrides
│   └── security        # a customized/extended Security Bundle
│       ├── models
│       ├── serializers
│       ├── services
│       ├── templates
│       └── __init__.py
├── db
│   └── migrations      # migrations generated by Flask-Migrate
├── static              # the top-level static assets folder
├── templates           # the top-level templates folder
└── tests               # your pytest tests

Want to start building now? Check out the Tutorial! There are also a couple open source example apps available:

Features

Bundles

Bundles are powerful and flexible. Conceptually, a bundle is a blueprint, and Flask Unchained gives you complete control to configure not only which views from each bundle get registered with your app and at what routes, but also to extend and/or override anything else you might want to from the bundles you enable.

Some examples of what you can customize from bundles include configuration, controllers, resources, and routes, templates, extensions and services, and models and serializers. Each uses simple and consistent patterns that work the same way across every bundle. Extended/customized bundles can themselves also be distributed as their own projects, and support the same patterns for customization, ad infinitum.

Bundle Structure

The example “hello world” app bundle lived in a single file, while a “full” bundle package typically consists of many modules (as shown just above under Project Layout). The module locations for your code are customizable on a per-bundle basis by setting class attributes on your Bundle subclass, for example:

# your_custom_bundle/__init__.py

from flask_unchained import Bundle

class YourCustomBundle(Bundle):
    config_module_name = 'settings'
    routes_module_name = 'urls'
    views_module_names = ['controllers', 'resources', 'views']

You can see the default module names and the override attribute names to set on your Bundle subclass by printing the ordered list of hooks that will run for your app using flask unchained hooks:

flask unchained hooks

Hook Name             Default Bundle Module  Bundle Module Override Attr
-------------------------------------------------------------------------
register_extensions   extensions             extensions_module_names
models                models                 models_module_names
configure_app         config                 config_module_name
init_extensions       extensions             extensions_module_names
services              services               services_module_names
commands              commands               commands_module_names
routes                routes                 routes_module_name
bundle_blueprints     (None)                 (None)
blueprints            views                  blueprints_module_names
views                 views                  views_module_names
model_serializers     serializers            model_serializers_module_names
model_resources       views                  model_resources_module_names
celery_tasks          tasks                  celery_tasks_module_names

Bundle Blueprints

Bundles are blueprints, so if you want to define request/response functions that should only run for views from a specific bundle, you can do that like so:

from flask_unchained import Bundle, unchained

class YourCoolBundle(Bundle):
    name = 'your_cool_bundle'  # the default (snake_cased class name)

@unchained.your_cool_bundle.before_request
def this_only_runs_before_requests_to_views_from_your_cool_bundle():
    pass

# the other supported decorators are also available:
@unchained.your_cool_bundle.after_request
@unchained.your_cool_bundle.teardown_request
@unchained.your_cool_bundle.context_processor
@unchained.your_cool_bundle.url_defaults
@unchained.your_cool_bundle.url_value_preprocessor
@unchained.your_cool_bundle.errorhandler

The API here is the same as flask.Blueprint, however, its methods must be accessed via the Unchained extension. The syntax is @unchained.bundle_name.blueprint_method_name.

Wait but why?

Sadly, there are some very serious technical limitations with the implementation of flask.Blueprint such that its direct usage breaks the power and flexibility of Flask Unchained views. Under the hood, Flask Unchained does indeed use an instance of flask.Blueprint for each bundle - you just never interact with them directly.

You can technically continue using flask.Blueprint strictly for views in your app bundle, however this support is only kept around for porting purposes. Note that even in your app bundle, views from blueprints unfortunately will not work with declarative routing.

Extending and Overriding Bundles

Extending and overriding bundles is pretty simple. All you need to do is subclass the bundle you want to extend in its own Python package, and include that package in your unchained_config.BUNDLES instead of the original bundle. There is no limit to the depth of the bundle hierarchy (other than perhaps your sanity). So, for example, to extend the Security Bundle, it would look like this:

# project-root/bundles/security/__init__.py

from flask_unchained.bundles.security import SecurityBundle as BaseSecurityBundle

class SecurityBundle(BaseSecurityBundle):
    pass
# project-root/unchained_config.py

BUNDLES = [
    # ...
    'bundles.security',
    'app',
]

The App Bundle

When defining the app bundle, you must subclass AppBundle instead of Bundle:

# project-root/app/__init__.py

from flask_unchained import AppBundle

class App(AppBundle):
    pass

Everything about your app bundle is otherwise the same as for regular bundles, except the app bundle can extend and/or override anything from any bundle.

Controllers and Templates

The controller bundle includes two base classes that all of your views should extend. The first is Controller, which under the hood is actually very similar to flask.views.View, however, they’re not compatible. The second is Resource, which extends Controller, and whose implementation draws much inspiration from Flask-RSETful (specifically, the Resource and Api classes).

Controller

Chances are Controller is the base class you want to extend, unless you’re building a RESTful API. Controllers include a bit of magic:

# your_bundle/views.py

from flask_unchained import Controller, route, injectable

 class SiteController(Controller):
     # all of class Meta is optional (automatic defaults shown)
     class Meta:
         abstract: bool = False
         url_prefix = Optional[str] = '/'          # aka no prefix
         endpoint_prefix: str = 'site_controller'  # snake_cased class name
         template_folder: str = 'site'             # snake_cased class name prefix
         template_file_extension: Optional[str] = '.html'
         decorators: List[callable] = ()

     # controllers automatically support dependency injection
     name_service: NameService = injectable

     @route('/foobaz', methods=['GET', 'POST'])
     def foo_baz():
         # template paths can be explicit
         return self.render('site/foo_baz.html')

     def view_one():
         # or just the filename
         return self.render('one')  # equivalent to 'site/one.html'

     def view_two():
         return self.render('two')

     def _protected_function():
         return 'not a view'

On any subclass of Controller that isn’t abstract, all public methods are automatically assigned default routing rules. In the example above, foo_baz has a route decorator, but view_one and view_two do not. The undecorated views will be assigned default routing rules of /view-one and /view-two respectively (the default is to convert the method name to kebab-case). Protected methods (those prefixed with _) are not assigned routes.

Templates

Flask Unchained uses the Jinja templating language, just like Flask.

By default bundles are configured to use a templates subfolder. This is customizable per-bundle:

# your_bundle/__init__.py

from flask_unchained import Bundle

class YourBundle(Bundle):
    template_folder = 'templates'  # the default

Controllers each have their own template folder within Bundle.template_folder. It defaults to the snake_cased class name, with the suffixes Controller or View stripped (if any). You can customize it using Controller.Meta.template_folder.

The default file extension used for templates is configured by setting TEMPLATE_FILE_EXTENSION in your app config. It defaults to .html, and is also configurable on a per-controller basis by setting Controller.Meta.template_file_extension.

Therefore, the above controller corresponds to the following templates folder structure:

./your_bundle
├── templates
│   └── site
│       ├── foo_baz.html
│       ├── one.html
│       └── two.html
├── __init__.py
└── views.py

Extending and Overriding Templates

Templates can be overridden by placing an equivalently named template higher up in the bundle hierarchy (i.e. in a bundle extending another bundle, or in your app bundle).

So for example, the Security Bundle includes default templates for all of its views. They are located at security/login.html, security/register.html, and so on. Thus, to override them, you would make a security folder in your app bundle’s templates folder and put your customized templates with the same names in it. You can even extend the template you’re overriding, using the standard Jinja syntax (this doesn’t work in regular Flask apps):

{# your_app_or_security_bundle/templates/security/login.html #}

{% extends 'security/login.html' %}

{% block content %}
   <h1>Login</h1>
   {{ render_form(login_user_form, endpoint='security_controller.login') }}
{% endblock %}

If you encounter problems, you can set the EXPLAIN_TEMPLATE_LOADING config option to True to help debug what’s going on.

Resources (API Controllers)

The Resource class extends Controller to add support for building RESTful APIs. It adds a bit of magic around specific methods:

Method name on your Resource subclass

HTTP Method

URL Rule

list

GET

/

create

POST

/

get

GET

/<cls.Meta.member_param>

patch

PATCH

/<cls.Meta.member_param>

put

PUT

/<cls.Meta.member_param>

delete

DELETE

/<cls.Meta.member_param>

If you implement any of these methods, then the shown URL rules will automatically be used.

So, for example:

from http import HTTPStatus
from flask_unchained import Resource, injectable, param_converter, request
from flask_unchained.bundles.security import User, UserManager

class UserResource(Resource):
    # class Meta is optional on resources (automatic defaults shown)
    class Meta:
        url_prefix = '/users'
        member_param = '<int:id>'
        unique_member_param = '<int:user_id>'

    # resources are controllers, so they support dependency injection
    user_manager: UserManager = injectable

    def list():
        return self.jsonify(dict(users=self.user_manager.all()))
        # NOTE: returning SQLAlchemy models directly like this is
        # only supported by ModelResource from the API Bundle

    def create():
        data = request.get_json()
        user = self.user_manager.create(**data, commit=True)
        return self.jsonify(dict(user=user), code=HTTPStatus.CREATED)

    @param_converter(id=User)
    def get(user):
        return self.jsonify(dict(user=user)

    @param_converter(id=User)
    def patch(user):
        data = request.get_json()
        user = self.user_manager.update(user, **data, commit=True)
        return self.jsonify(dict(user=user))

    @param_converter(id=User)
    def put(user):
        data = request.get_json()
        user = self.user_manager.update(user, **data, commit=True)
        return self.jsonify(dict(user=user))

    @param_converter(id=User)
    def delete(user):
        self.user_manager.delete(user, commit=True)
        return self.make_response('', code=HTTPStatus.NO_CONTENT)

Registered like so:

routes = lambda: [
    resource(UserResource),
]

Results in the following routes:

GET     /users             UserResource.list
POST    /users             UserResource.create
GET     /users/<int:id>    UserResource.get
PATCH   /users/<int:id>    UserResource.patch
PUT     /users/<int:id>    UserResource.put
DELETE  /users/<int:id>    UserResource.delete

Declarative Routing

Using declarative routing, your app bundle has final say over which views (from all bundles) should get registered with the app, as well as their routing rules. By default, it uses the rules decorated on views:

# project-root/app/routes.py

from flask_unchained import (controller, resource, func, include, prefix,
                             delete, get, patch, post, put, rule)

from flask_unchained.bundles.security import SecurityController

from .views import SiteController

routes = lambda: [
    controller(SiteController),
    controller(SecurityController),
]

By running flask urls, we can verify it does what we want:

flask urls
Method(s)  Rule                     Endpoint                    View
---------------------------------------------------------------------------------------------------------------------------------
      GET  /static/<path:filename>  static                      flask.helpers.send_static_file
      GET  /                        site_controller.index       app.views.SiteController.index
      GET  /hello                   site_controller.hello       app.views.SiteController.hello
GET, POST  /login                   security_controller.login   flask_unchained.bundles.security.views.SecurityController.login
      GET  /logout                  security_controller.logout  flask_unchained.bundles.security.views.SecurityController.logout

Declarative routing can also be much more powerful when you want it to be. For example, to build a RESTful SPA with the Security Bundle, your routes might look like this:

# project-root/app/routes.py

from flask_unchained import (controller, resource, func, include, prefix,
                             delete, get, patch, post, put, rule)

from flask_unchained.bundles.security import SecurityController, UserResource

from .views import SiteController

routes = lambda: [
    controller(SiteController),

    controller('/auth', SecurityController, rules=[
        get('/reset-password/<token>', SecurityController.reset_password,
            endpoint='security_api.reset_password'),
    ]),
    prefix('/api/v1', [
        controller('/auth', SecurityController, rules=[
            get('/check-auth-token', SecurityController.check_auth_token,
                endpoint='security_api.check_auth_token', only_if=True),
            post('/login', SecurityController.login,
                 endpoint='security_api.login'),
            get('/logout', SecurityController.logout,
                endpoint='security_api.logout'),
            post('/send-confirmation-email',
                 SecurityController.send_confirmation_email,
                 endpoint='security_api.send_confirmation_email'),
            post('/forgot-password', SecurityController.forgot_password,
                 endpoint='security_api.forgot_password'),
            post('/reset-password/<token>', SecurityController.reset_password,
                 endpoint='security_api.post_reset_password'),
            post('/change-password', SecurityController.change_password,
                 endpoint='security_api.change_password'),
        ]),
        resource('/users', UserResource),
    ]),
]

Which results in the following:

flask urls
Method(s)  Rule                                  Endpoint                              View
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
      GET  /static/<path:filename>               static                                flask.helpers.send_static_file
      GET  /                                     site_controller.index                 app.views.SiteController.index
      GET  /hello                                site_controller.hello                 app.views.SiteController.hello
      GET  /auth/reset-password/<token>          security_api.reset_password           flask_unchained.bundles.security.views.SecurityController.reset_password
      GET  /api/v1/auth/check-auth-token         security_api.check_auth_token         flask_unchained.bundles.security.views.SecurityController.check_auth_token
     POST  /api/v1/auth/login                    security_api.login                    flask_unchained.bundles.security.views.SecurityController.login
      GET  /api/v1/auth/logout                   security_api.logout                   flask_unchained.bundles.security.views.SecurityController.logout
     POST  /api/v1/auth/send-confirmation-email  security_api.send_confirmation_email  flask_unchained.bundles.security.views.SecurityController.send_confirmation_email
     POST  /api/v1/auth/forgot-password          security_api.forgot_password          flask_unchained.bundles.security.views.SecurityController.forgot_password
     POST  /api/v1/auth/reset-password/<token>   security_api.post_reset_password      flask_unchained.bundles.security.views.SecurityController.reset_password
     POST  /api/v1/auth/change-password          security_api.change_password          flask_unchained.bundles.security.views.SecurityController.change_password
     POST  /api/v1/users                         user_resource.create                  flask_unchained.bundles.security.views.UserResource.create
      GET  /api/v1/users/<int:id>                user_resource.get                     flask_unchained.bundles.security.views.UserResource.get
    PATCH  /api/v1/users/<int:id>                user_resource.patch                   flask_unchained.bundles.security.views.UserResource.patch

Here is a summary of the functions imported at the top of the routes.py module:

Declarative Routing Functions

Function

Description

include()

Include all of the routes from the specified module at that point in the tree.

prefix()

Prefixes all of the child routing rules with the given prefix.

func()

Registers a function-based view with the app, optionally specifying the routing rules.

controller()

Registers a controller and its views with the app, optionally customizing the routes to register.

resource()

Registers a resource and its views with the app, optionally customizing the routes to register.

rule()

Define customizations to a controller/resource method’s route rules.

get(), patch(), post(), put(), and delete()

Like rule() except specifically for each HTTP method.

Dependency Injection

Flask Unchained supports dependency injection of services and extensions (by default).

Services

For services to be automatically discovered, they must subclass Service and (by default) live in a bundle’s services or managers modules. You can however manually register anything as a “service”, even plain values if you really wanted to, using the unchained.service decorator and/or the unchained.register_service method:

from flask_unchained import unchained

@unchained.service(name='something')
class SomethingNotExtendingService:
    pass

A_CONST = 'a constant'
unchained.register_service('A_CONST', A_CONST)

Services can request other services be injected into them, and as long as there are no circular dependencies, it will work:

from flask_unchained import Service, injectable

class OneService(Service):
    something: SomethingNotExtendingService = injectable
    A_CONST: str = injectable

class TwoService(Service):
    one_service: OneService = injectable

By setting the default value of a class attribute or function/method argument to the flask_unchained.injectable constant, you are informing the Unchained extension that it should inject those arguments.

Important

The names of services must be unique across all of the bundles in your app (by default services are named as the snake_cased class name). If there are any conflicting class names then you will need to use the unchained.service decorator or the unchained.register_service method to customize the name the service gets registered under:

from flask_unchained import Service, unchained

@unchained.service('a_unique_name')
class ServiceWithNameConflict(Service):
    pass

Automatic Dependency Injection

Dependency injection works automatically on all classes extending Service and Controller. The easiest way is with class attributes:

from flask_unchained import Controller, injectable
from flask_unchained.bundles.security import Security, SecurityService
from flask_unchained.bundles.sqlalchemy import SessionManager

class SecurityController(Controller):
    security: Security = injectable
    security_service: SecurityService = injectable
    session_manager: SessionManager = injectable

It also works on the constructor, which is functionally equivalent, just more verbose:

class SiteController(Controller):
    def __init__(self, security: Security = injectable):
        self.security = security

Manual Dependency Injection

You can use the unchained.inject decorator just about anywhere else you want to inject something:

from flask_unchained import unchained, injectable

# decorate a class to use class attributes injection
@unchained.inject()
class Foobar:
    some_service: SomeService = injectable

    # or you can decorate individual methods
    @unchained.inject()
    def a_method(self, another_service: AnotherService = injectable):
        pass

# it works on regular functions too
@unchained.inject()
def a_function(some_service: SomeService = injectable):
    pass

Alternatively, you can also use unchained.get_local_proxy:

from flask_unchained import unchained

db = unchained.get_local_proxy('db')

Extending and Overriding Services

Services are just classes, so they follow the normal Python inheritance rules. All you need to do is name your service the same as the one you want to customize, placed in the services module higher up in the bundle hierarchy (i.e. in a bundle extending another bundle, or in your app bundle).

Integrating Flask Extensions

Extensions that can be used in Flask Unchained bundles have a few limitations. The primary one being, the extension must implement init_app, and its signature must take a single argument: app. Some extensions fit this restriction out of the box, but often times you will need to subclass the extension to make sure its init_app signature matches. You can create new config options to replace arguments that were originally passed into the extension’s constructor and/or init_app method.

In order for Flask Unchained to actually discover and initialize the extension you want to include, they must be placed in your bundle’s extensions module. It looks like this:

# your_bundle/extensions.py

from flask_whatever import WhateverExtension

whatever = WhateverExtension()

EXTENSIONS = {
    'whatever': whatever,
}

The keys of the EXTENSIONS dictionary serve as the name that will be used to reference the extension at runtime (and for dependency injection). There can be multiple extensions per bundle, and you can also declare other extensions as dependencies that must be initialized before yours:

EXTENSIONS = {
    'whatever': (whatever, ['dep_ext_one', 'dep_ext_two']),
}