Flask + Dependency Injector - dependency injection guide

Hi,



I am the creator of Dependency Injector . This is a dependency injection framework for Python.



In this tutorial, I want to show how to use the Dependency Injector to develop Flask applications.



The manual consists of the following parts:



  1. What are we going to build?
  2. Prepare the environment
  3. Project structure
  4. Hello world!
  5. Including styles
  6. Connecting Github
  7. Search service
  8. Connect search
  9. A bit of refactoring
  10. Adding tests
  11. Conclusion


The completed project can be found on Github .



To start you must have:



  • Python 3.5+
  • Virtual environment


And it is desirable to have:



  • Initial development skills with Flask
  • Understanding the principle of dependency injection


What are we going to build?



We will be building an application that helps you search for repositories on Github. Let's call it Github Navigator.



How does Github Navigator work?



  • The user opens a web page where he is prompted to enter a search query.
  • The user enters a query and presses Enter.
  • The Github Navigator looks for matching repositories on Github.
  • When the search finishes, the Github Navigator shows the user a web page with results.
  • The results page shows all found repositories and the search query.
  • For each repository, the user sees:

    • repository name
    • repository owner
    • the last commit to the repository
  • The user can click on any of the elements to open his page on Github.






Prepare the environment



First of all, we need to create a project folder and a virtual environment:



mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv


Now let's activate the virtual environment:



. venv/bin/activate


The environment is ready, now let's start with the project structure.



Project structure



Let's create the following structure in the current folder. Leave all files empty for now. This is not yet critical.



Initial structure:



./
β”œβ”€β”€ githubnavigator/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ application.py
β”‚   β”œβ”€β”€ containers.py
β”‚   └── views.py
β”œβ”€β”€ venv/
└── requirements.txt


It's time to install Flask and Dependency Injector.



Let's add the following lines to the file requirements.txt:



dependency-injector
flask


Now let's install them:



pip install -r requirements.txt


And check that the installation was successful:



python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"


You will see something like:



(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2


Hello world!



Let's create a minimal hello world app.



Let's add the following lines to the file views.py:



"""Views module."""


def index():
    return 'Hello, World!'


Now let's add a container for dependencies (hereinafter just a container). The container will contain all the components of the application. Let's add the first two components. This is a Flask application and view index.



Let's add the following to the file containers.py:



"""Application containers module."""

from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask

from . import views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    index_view = flask.View(views.index)


Now we need to create a Flask application factory. It is usually called create_app(). It will create a container. The container will be used to create the Flask application. The final step is to set up the routing - we will assign the view index_viewfrom the container to handle requests to the root "/" of our application.



Let's edit application.py:



"""Application module."""

from .containers import ApplicationContainer


def create_app():
    """Create and return Flask application."""
    container = ApplicationContainer()

    app = container.app()
    app.container = container

    app.add_url_rule('/', view_func=container.index_view.as_view())

    return app


The container is the first object in the application. It is used to get all other objects.


Our application is now ready to say "Hello, World!"



Run in a terminal:



export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run


The output should look something like this:



* Serving Flask app "githubnavigator.application" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 473-587-859


Open your browser and go to http://127.0.0.1:5000/ .



You will see "Hello, World!"



Excellent. Our minimal application starts and runs successfully.



Let's make it a little prettier.



Including styles



We will be using Bootstrap 4 . Let's use the Bootstrap-Flask extension for this . It will help us add all the necessary files in a few clicks.



Add bootstrap-flaskto requirements.txt:



dependency-injector
flask
bootstrap-flask


and execute in the terminal:



pip install --upgrade -r requirements.txt


Now let's add the extension bootstrap-flaskto the container.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap

from . import views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    index_view = flask.View(views.index)


Let's initialize the extension bootstrap-flask. We will need to change create_app().



Edit application.py:



"""Application module."""

from .containers import ApplicationContainer


def create_app():
    """Create and return Flask application."""
    container = ApplicationContainer()

    app = container.app()
    app.container = container

    bootstrap = container.bootstrap()
    bootstrap.init_app(app)

    app.add_url_rule('/', view_func=container.index_view.as_view())

    return app


Now we need to add templates. To do this, we need to add a folder templates/to the package githubnavigator. Add two files inside the templates folder:



  • base.html - basic template
  • index.html - main page template


Create a folder templatesand two empty files inside base.htmland index.html:



./
β”œβ”€β”€ githubnavigator/
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   β”œβ”€β”€ base.html
β”‚   β”‚   └── index.html
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ application.py
β”‚   β”œβ”€β”€ containers.py
β”‚   └── views.py
β”œβ”€β”€ venv/
└── requirements.txt


Now let's fill in the basic template.



Let's add the following lines to the file base.html:



<!doctype html>
<html lang="en">
    <head>
        {% block head %}
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        {% block styles %}
            <!-- Bootstrap CSS -->
            {{ bootstrap.load_css() }}
        {% endblock %}

        <title>{% block title %}{% endblock %}</title>
        {% endblock %}
    </head>
    <body>
        <!-- Your page content -->
        {% block content %}{% endblock %}

        {% block scripts %}
            <!-- Optional JavaScript -->
            {{ bootstrap.load_js() }}
        {% endblock %}
    </body>
</html>


Now let's fill in the master page template.



Let's add the following lines to the file index.html:



{% extends "base.html" %}

{% block title %}Github Navigator{% endblock %}

{% block content %}
<div class="container">
    <h1 class="mb-4">Github Navigator</h1>

    <form>
        <div class="form-group form-row">
            <div class="col-10">
                <label for="search_query" class="col-form-label">
                    Search for:
                </label>
                <input class="form-control" type="text" id="search_query"
                       placeholder="Type something to search on the GitHub"
                       name="query"
                       value="{{ query if query }}">
            </div>
            <div class="col">
                <label for="search_limit" class="col-form-label">
                    Limit:
                </label>
                <select class="form-control" id="search_limit" name="limit">
                    {% for value in [5, 10, 20] %}
                    <option {% if value == limit %}selected{% endif %}>
                        {{ value }}
                    </option>
                    {% endfor %}
                </select>
            </div>
        </div>
    </form>

    <p><small>Results found: {{ repositories|length }}</small></p>

    <table class="table table-striped">
        <thead>
            <tr>
                <th>#</th>
                <th>Repository</th>
                <th class="text-nowrap">Repository owner</th>
                <th class="text-nowrap">Last commit</th>
            </tr>
        </thead>
        <tbody>
        {% for repository in repositories %} {{n}}
            <tr>
              <th>{{ loop.index }}</th>
              <td><a href="{{ repository.url }}">
                  {{ repository.name }}</a>
              </td>
              <td><a href="{{ repository.owner.url }}">
                  <img src="{{ repository.owner.avatar_url }}"
                       alt="avatar" height="24" width="24"/></a>
                  <a href="{{ repository.owner.url }}">
                      {{ repository.owner.login }}</a>
              </td>
              <td><a href="{{ repository.latest_commit.url }}">
                  {{ repository.latest_commit.sha }}</a>
                  {{ repository.latest_commit.message }}
                  {{ repository.latest_commit.author_name }}
              </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
</div>

{% endblock %}


Great, almost done. The final step is indexto change the view to use the template index.html.



Let's edit views.py:



"""Views module."""

from flask import request, render_template


def index():
    query = request.args.get('query', 'Dependency Injector')
    limit = request.args.get('limit', 10, int)

    repositories = []

    return render_template(
        'index.html',
        query=query,
        limit=limit,
        repositories=repositories,
    )


Done.



Make sure the application is running or run flask runand open http://127.0.0.1:5000/ .



You should see:







Connecting Github



In this section, we will integrate our application with the Github API.

We will be using the PyGithub library .



Let's add it to requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub


and execute in the terminal:



pip install --upgrade -r requirements.txt


Now we need to add the Github API client to the container. To do this, we will need to use two new providers from the module dependency_injector.providers:



  • The provider Factorywill create the Github client.
  • The provider Configurationwill pass the API token and Github timeout to the client.


Let's do it.



Let's edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    index_view = flask.View(views.index)


We used the configuration parameters before setting their values. This is the principle by which the provider works Configuration.



First we use, then we set the values.



Now let's add the configuration file.

We will use YAML.



Create an empty file config.ymlat the root of the project:



./
β”œβ”€β”€ githubnavigator/
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   β”œβ”€β”€ base.html
β”‚   β”‚   └── index.html
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ application.py
β”‚   β”œβ”€β”€ containers.py
β”‚   └── views.py
β”œβ”€β”€ venv/
β”œβ”€β”€ config.yml
└── requirements.txt


And fill it in with the following lines:



github:
  request_timeout: 10


To work with the configuration file, we will use the PyYAML library . Let's add it to the file with dependencies.



Edit requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub
pyyaml


and install the dependency:



pip install --upgrade -r requirements.txt


We'll use an environment variable to pass the API token GITHUB_TOKEN.



Now we need to edit create_app()to do 2 actions when the application starts:



  • Load configuration from config.yml
  • Load API token from environment variable GITHUB_TOKEN


Edit application.py:



"""Application module."""

from .containers import ApplicationContainer


def create_app():
    """Create and return Flask application."""
    container = ApplicationContainer()
    container.config.from_yaml('config.yml')
    container.config.github.auth_token.from_env('GITHUB_TOKEN')

    app = container.app()
    app.container = container

    bootstrap = container.bootstrap()
    bootstrap.init_app(app)

    app.add_url_rule('/', view_func=container.index_view.as_view())

    return app


Now we need to create an API token.



For this you need:



  • Follow this tutorial on Github
  • Set token to environment variable:



    export GITHUB_TOKEN=<your token>


This item can be temporarily skipped.



The app will run without a token, but with limited bandwidth. Limit for unauthenticated clients: 60 requests per hour. The token is needed to increase this quota to 5000 per hour.


Done.



The client Github API installation is complete.



Search service



It's time to add a search service SearchService. He will:



  • Search on Github
  • Get additional data about commits
  • Convert format result


SearchServicewill use the Github API client.



Create an empty file services.pyin the package githubnavigator:



./
β”œβ”€β”€ githubnavigator/
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   β”œβ”€β”€ base.html
β”‚   β”‚   └── index.html
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ application.py
β”‚   β”œβ”€β”€ containers.py
β”‚   β”œβ”€β”€ services.py
β”‚   └── views.py
β”œβ”€β”€ venv/
β”œβ”€β”€ config.yml
└── requirements.txt


and add the following lines to it:



"""Services module."""

from github import Github
from github.Repository import Repository
from github.Commit import Commit


class SearchService:
    """Search service performs search on Github."""

    def __init__(self, github_client: Github):
        self._github_client = github_client

    def search_repositories(self, query, limit):
        """Search for repositories and return formatted data."""
        repositories = self._github_client.search_repositories(
            query=query,
            **{'in': 'name'},
        )
        return [
            self._format_repo(repository)
            for repository in repositories[:limit]
        ]

    def _format_repo(self, repository: Repository):
        commits = repository.get_commits()
        return {
            'url': repository.html_url,
            'name': repository.name,
            'owner': {
                'login': repository.owner.login,
                'url': repository.owner.html_url,
                'avatar_url': repository.owner.avatar_url,
            },
            'latest_commit': self._format_commit(commits[0]) if commits else {},
        }

    def _format_commit(self, commit: Commit):
        return {
            'sha': commit.sha,
            'url': commit.html_url,
            'message': commit.commit.message,
            'author_name': commit.commit.author.name,
        }


Now let's add SearchServiceto the container.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.View(views.index)


Connect search



We are now ready for the search to work. Let's use SearchServicein indexview.



Edit views.py:



"""Views module."""

from flask import request, render_template

from .services import SearchService


def index(search_service: SearchService):
    query = request.args.get('query', 'Dependency Injector')
    limit = request.args.get('limit', 10, int)

    repositories = search_service.search_repositories(query, limit)

    return render_template(
        'index.html',
        query=query,
        limit=limit,
        repositories=repositories,
    )


Now let's change the container to pass the dependency SearchServiceto the view indexwhen it is called.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.View(
        views.index,
        search_service=search_service,
    )


Make sure the application is running or run flask runand open http://127.0.0.1:5000/ .



You will see:







A bit of refactoring



Our view indexcontains two hardcoded values:



  • Default search term
  • Limit for the number of results


Let's do a little refactoring. We will transfer these values ​​into the configuration.



Edit views.py:



"""Views module."""

from flask import request, render_template

from .services import SearchService


def index(
        search_service: SearchService,
        default_query: str,
        default_limit: int,
):
    query = request.args.get('query', default_query)
    limit = request.args.get('limit', default_limit, int)

    repositories = search_service.search_repositories(query, limit)

    return render_template(
        'index.html',
        query=query,
        limit=limit,
        repositories=repositories,
    )


Now we need these values ​​to be passed on call. Let's update the container.



Edit containers.py:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.View(
        views.index,
        search_service=search_service,
        default_query=config.search.default_query,
        default_limit=config.search.default_limit,
    )


Now let's update the config file.



Edit config.yml:



github:
  request_timeout: 10
search:
  default_query: "Dependency Injector"
  default_limit: 10


Done.



The refactoring is complete. Mu made the code cleaner.



Adding tests



It would be nice to add some tests. Let's do it.



We'll be using pytest and coverage .



Edit requirements.txt:



dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov


and install new packages:



pip install -r requirements.txt


Create an empty file tests.pyin the package githubnavigator:



./
β”œβ”€β”€ githubnavigator/
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   β”œβ”€β”€ base.html
β”‚   β”‚   └── index.html
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ application.py
β”‚   β”œβ”€β”€ containers.py
β”‚   β”œβ”€β”€ services.py
β”‚   β”œβ”€β”€ tests.py
β”‚   └── views.py
β”œβ”€β”€ venv/
β”œβ”€β”€ config.yml
└── requirements.txt


and add the following lines to it:



"""Tests module."""

from unittest import mock

import pytest
from github import Github
from flask import url_for

from .application import create_app


@pytest.fixture
def app():
    return create_app()


def test_index(client, app):
    github_client_mock = mock.Mock(spec=Github)
    github_client_mock.search_repositories.return_value = [
        mock.Mock(
            html_url='repo1-url',
            name='repo1-name',
            owner=mock.Mock(
                login='owner1-login',
                html_url='owner1-url',
                avatar_url='owner1-avatar-url',
            ),
            get_commits=mock.Mock(return_value=[mock.Mock()]),
        ),
        mock.Mock(
            html_url='repo2-url',
            name='repo2-name',
            owner=mock.Mock(
                login='owner2-login',
                html_url='owner2-url',
                avatar_url='owner2-avatar-url',
            ),
            get_commits=mock.Mock(return_value=[mock.Mock()]),
        ),
    ]

    with app.container.github_client.override(github_client_mock):
        response = client.get(url_for('index'))

    assert response.status_code == 200
    assert b'Results found: 2' in response.data

    assert b'repo1-url' in response.data
    assert b'repo1-name' in response.data
    assert b'owner1-login' in response.data
    assert b'owner1-url' in response.data
    assert b'owner1-avatar-url' in response.data

    assert b'repo2-url' in response.data
    assert b'repo2-name' in response.data
    assert b'owner2-login' in response.data
    assert b'owner2-url' in response.data
    assert b'owner2-avatar-url' in response.data


def test_index_no_results(client, app):
    github_client_mock = mock.Mock(spec=Github)
    github_client_mock.search_repositories.return_value = []

    with app.container.github_client.override(github_client_mock):
        response = client.get(url_for('index'))

    assert response.status_code == 200
    assert b'Results found: 0' in response.data


Now let's start testing and check coverage:



py.test githubnavigator/tests.py --cov=githubnavigator


You will see:



platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: flask-1.0.0, cov-2.10.0
collected 2 items

githubnavigator/tests.py ..                                     [100%]

---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name                             Stmts   Miss  Cover
----------------------------------------------------
githubnavigator/__init__.py          0      0   100%
githubnavigator/application.py      11      0   100%
githubnavigator/containers.py       13      0   100%
githubnavigator/services.py         14      0   100%
githubnavigator/tests.py            32      0   100%
githubnavigator/views.py             7      0   100%
----------------------------------------------------
TOTAL                               77      0   100%


Notice how we replace github_clientwith mock using the method .override(). In this way, you can override the return value of any provider.



Conclusion



We built a Flask application using dependency injection. We used Dependency Injector as a dependency injection framework.



The main part of our application is the container. It contains all the components of the application and their dependencies in one place. This provides control over the structure of the application. It is easy to understand and change:



"""Application containers module."""

from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github

from . import services, views


class ApplicationContainer(containers.DeclarativeContainer):
    """Application container."""

    app = flask.Application(Flask, __name__)

    bootstrap = flask.Extension(Bootstrap)

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.github.auth_token,
        timeout=config.github.request_timeout,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

    index_view = flask.View(
        views.index,
        search_service=search_service,
        default_query=config.search.default_query,
        default_limit=config.search.default_limit,
    )




A container as a map of your application. You always know what depends on what.



What's next?






All Articles