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:
- What are we going to build?
- Prepare the environment
- Project structure
- Hello world!
- Including styles
- Connecting Github
- Search service
- Connect search
- A bit of refactoring
- Adding tests
- 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_view
from 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-flask
to 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-flask
to 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 templateindex.html
- main page template
Create a folder
templates
and two empty files inside base.html
and 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
index
to 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 run
and 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
Factory
will create the Github client. - The provider
Configuration
will 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 worksConfiguration
.
First we use, then we set the values.
Now let's add the configuration file.
We will use YAML.
Create an empty file
config.yml
at 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
SearchService
will use the Github API client.
Create an empty file
services.py
in 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
SearchService
to 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
SearchService
in index
view.
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
SearchService
to the view index
when 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 run
and open http://127.0.0.1:5000/ .
You will see:
A bit of refactoring
Our view
index
contains 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.py
in 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 replacegithub_client
with 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?
- Learn more about Dependency Injector on GitHub
- Check out the documentation at Read the Docs
- Have a question or find a bug? Open an issue on Github