.. BEGIN setup/comments ------------------------------------------------------- The heading hierarchy is defined as: h1: = h2: - h3: ^ h4: ~ h5: " h6: # .. BEGIN document ------------------------------------------------------------- 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** :bolditalic:`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+** .. code:: shell pip install "flask-unchained[dev]" A Basic App ^^^^^^^^^^^ Almost as simple as it gets: .. code-block:: # 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: .. code-block:: bash cd project-root export UNCHAINED_CONFIG="app" pytest flask run .. admonition:: Included bundles & integrated extensions :class: tip - **Controller Bundle**: Enhanced class-based views, declarative routing, and `Flask WTF `_ for forms and CSRF protection. - **SQLAlchemy Bundle** `Flask SQLAlchemy `_ and `Flask Migrate `_ for models and migrations, plus some optional "sugar" on top of `SQLAlchemy `_ to make the best ORM in existence even quicker and easier to use with `SQLAlchemy Unchained `_. - **Security Bundle**: `Flask Login `_ for authentication and `Flask Principal `_ for authorization. - **API Bundle**: RESTful APIs with `Flask Marshmallow `_ serializers for SQLAlchemy models. - **Graphene Bundle**: `Flask GraphQL `_ with `Graphene `_ for SQLAlchemy models. - **Celery Bundle**: `Celery `_ distributed tasks queue. - `Flask Admin `_, `Flask BabelEx `_, `Flask Mail `_, `Flask Session `_, ... - **Testing** with `pytest `_ and `factory_boy `_ supported out-of-the-box. - Don't like the default stack? With Flask Unchained you can bring your own. Flask Unchained is designed to be so flexible that you could even use it to create your own works-out-of-the-box web framework for Flask. 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) .. admonition:: New in v0.8: asyncio support *[experimental]* :class: danger Thanks to `Quart `_, Flask Unchained now has **preliminary** support for building asynchronous web apps with asyncio: .. code:: bash pip install "flask-unchained[dev]" quart .. code-block:: # 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}') .. code:: html+jinja Websocket example
    .. code:: bash 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 ;) .. admonition:: Disclaimer: this is alpha/beta quality software :class: warning 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! .. admonition:: Thanks and acknowledgements :class: tip 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``: .. code-block:: # 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: .. code-block:: 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: .. code-block:: # project-root/wsgi.py from flask_unchained import AppFactory, PROD app = AppFactory().create_app(env=PROD) For a deeper look check out :doc:`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: .. code-block:: shell /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 :ref:`tutorial`! There are also a couple open source example apps available: * `Flask Unchained React SPA `_ * `Flask Techan Unchained `_ * Open a PR to add yours! 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 :class:`~flask_unchained.bundles.Bundle` subclass, for example: .. code-block:: # 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 :class:`~flask_unchained.bundles.Bundle` subclass by printing the ordered list of hooks that will run for your app using ``flask unchained hooks``: .. code-block:: shell 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: .. code-block:: 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 :class:`flask.Blueprint`, however, its methods must be accessed via the :class:`~flask_unchained.unchained.Unchained` extension. The syntax is ``@unchained.bundle_name.blueprint_method_name``. .. admonition:: Wait but why? :class: warning Sadly, there are some very serious technical limitations with the implementation of :class:`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 :class:`flask.Blueprint` for each bundle - you just never interact with them directly. You can *technically* continue using :class:`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: .. code:: python # project-root/bundles/security/__init__.py from flask_unchained.bundles.security import SecurityBundle as BaseSecurityBundle class SecurityBundle(BaseSecurityBundle): pass .. code:: python # project-root/unchained_config.py BUNDLES = [ # ... 'bundles.security', 'app', ] The App Bundle ~~~~~~~~~~~~~~ When defining the app bundle, you must subclass :class:`~flask_unchained.bundles.AppBundle` instead of :class:`~flask_unchained.bundles.Bundle`: .. code-block:: # 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 :class:`~flask_unchained.Controller`, which under the hood is actually very similar to :class:`flask.views.View`, however, they're not compatible. The second is :class:`~flask_unchained.Resource`, which extends :class:`~flask_unchained.Controller`, and whose implementation draws much inspiration from `Flask-RSETful `_ (specifically, the `Resource `_ and `Api `_ classes). Controller ~~~~~~~~~~ Chances are :class:`~flask_unchained.Controller` is the base class you want to extend, unless you're building a RESTful API. Controllers include a bit of magic: .. code:: python # 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: .. code-block:: # 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 :attr:`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 :attr:`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 :attr:`Controller.Meta.template_file_extension`. Therefore, the above controller corresponds to the following templates folder structure: .. code-block:: shell ./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): .. code:: django {# your_app_or_security_bundle/templates/security/login.html #} {% extends 'security/login.html' %} {% block content %}

    Login

    {{ 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 :class:`~flask_unchained.Resource` class extends :class:`~flask_unchained.Controller` to add support for building RESTful APIs. It adds a bit of magic around specific methods: .. list-table:: :header-rows: 1 * - Method name on your Resource subclass - HTTP Method - URL Rule * - list - GET - / * - create - POST - / * - get - GET - / * - patch - PATCH - / * - put - PUT - / * - delete - DELETE - / 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 = '' unique_member_param = '' # 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/ UserResource.get PATCH /users/ UserResource.patch PUT /users/ UserResource.put DELETE /users/ 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: .. code-block:: # 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: .. code-block:: shell flask urls Method(s) Rule Endpoint View --------------------------------------------------------------------------------------------------------------------------------- GET /static/ 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: .. code-block:: # 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/', 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/', 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: .. code-block:: shell flask urls Method(s) Rule Endpoint View ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ GET /static/ 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/ 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/ 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/ user_resource.get flask_unchained.bundles.security.views.UserResource.get PATCH /api/v1/users/ 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: .. list-table:: Declarative Routing Functions :header-rows: 1 :widths: 20 80 * - Function - Description * - :func:`~flask_unchained.include` - Include all of the routes from the specified module at that point in the tree. * - :func:`~flask_unchained.prefix` - Prefixes all of the child routing rules with the given prefix. * - :func:`~flask_unchained.func` - Registers a function-based view with the app, optionally specifying the routing rules. * - :func:`~flask_unchained.controller` - Registers a controller and its views with the app, optionally customizing the routes to register. * - :func:`~flask_unchained.resource` - Registers a resource and its views with the app, optionally customizing the routes to register. * - :func:`~flask_unchained.rule` - Define customizations to a controller/resource method's route rules. * - :func:`~flask_unchained.get`, :func:`~flask_unchained.patch`, :func:`~flask_unchained.post`, :func:`~flask_unchained.put`, and :func:`~flask_unchained.delete` - Like :func:`~flask_unchained.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 :class:`~flask_unchained.di.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: .. code-block:: 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: .. code-block:: 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 :attr:`flask_unchained.injectable` constant, you are informing the :class:`~flask_unchained.Unchained` extension that it should inject those arguments. .. admonition:: Important :class: info 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 :class:`~flask_unchained.di.Service` and :class:`~flask_unchained.bundles.controller.controller.Controller`. The easiest way is with class attributes: .. code-block:: 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: .. code-block:: 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``: .. code-block:: 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: .. code:: python # 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: .. code:: python EXTENSIONS = { 'whatever': (whatever, ['dep_ext_one', 'dep_ext_two']), }