Flask tutorial¶
This tutorial shows how to build aFlask application following the dependency injectionprinciple.
Start from the scratch or jump to the section:
You can find complete project on theGithub.
What are we going to build?¶
We will build a web application that helps to search for repositories on the Github. Let’s call itGithub Navigator.
How does Github Navigator work?
User opens a web page that asks to provide a search query.
User types the query and hits Enter.
Github Navigator takes that and searches through the Github for matching repositories.
When search is done Github Navigator returns user a web page with the result.
The results page shows all matching repositories and the provided search query.
- For any matching repository user sees:
the repository name
the owner of the repository
the last commit to the repository
User can click on the repository, the repository owner or the last commit to open its web pageon the Github.

Prepare the environment¶
Let’s create the environment for the project.
First we need to create a project folder:
mkdirghnav-flask-tutorialcdghnav-flask-tutorialNow let’s create and activate virtual environment:
python3-mvenvvenv.venv/bin/activate
Project layout¶
Environment is ready and now we’re going to create the layout of the project.
Create next structure in the current directory. All files should be empty. That’s ok for now.
Initial project layout:
./├── githubnavigator/│ ├── __init__.py│ ├── application.py│ ├── containers.py│ └── views.py├── venv/└── requirements.txt
Now it’s time to installFlask andDependencyInjector.
Put next lines into therequirements.txt file:
dependency-injectorflask
Now let’s install it:
pipinstall-rrequirements.txt
And check that installation is successful:
python-c"import dependency_injector; print(dependency_injector.__version__)"python-c"import flask; print(flask.__version__)"
You should see something like:
(venv)$python-c"import dependency_injector; print(dependency_injector.__version__)"4.37.0(venv)$python-c"import flask; print(flask.__version__)"2.0.2
Versions can be different. That’s fine.
Hello World!¶
Let’s create minimal application.
Put next into theviews.py:
"""Views module."""defindex():return"Hello, World!"
Ok, we have the view.
Now let’s create a container. Container will keep all of the application components and their dependencies.
Editcontainers.py:
"""Containers module."""fromdependency_injectorimportcontainersclassContainer(containers.DeclarativeContainer):...
Container is empty for now. We will add the providers in the following sections.
Finally we need to create Flask application factory. It will create and configure containerand Flask application. It is traditionally calledcreate_app().We will assignindex view to handle user requests to the root/ of our web application.
Put next into theapplication.py:
"""Application module."""fromflaskimportFlaskfrom.containersimportContainerfrom.importviewsdefcreate_app()->Flask:container=Container()app=Flask(__name__)app.container=containerapp.add_url_rule("/","index",views.index)returnapp
Ok. Now we’re ready to say “Hello, World!”.
Do next in the terminal:
exportFLASK_APP=githubnavigator.applicationexportFLASK_ENV=developmentflaskrun
The output should be something like:
*ServingFlaskapp"githubnavigator.application"(lazyloading)*Environment:development*Debugmode:on*Runningonhttp://127.0.0.1:5000/(PressCTRL+Ctoquit)*Restartingwithfseventsreloader*Debuggerisactive!*DebuggerPIN:473-587-859
Open your browser and go to thehttp://127.0.0.1:5000/.
You should seeHello,World!.
That’s it. Our minimal application is up and running.
Make it pretty¶
Now let’s make it look pretty. We will useBootstrap 4.For adding it to our application we will getBootstrap-Flask extension.It will help us to add all needed static files in few clicks.
Addbootstrap-flask to therequirements.txt:
dependency-injectorflaskbootstrap-flaskand run in the terminal:
pipinstall-rrequirements.txt
Let’s initializebootstrap-flask extension. We will need to modifycreate_app().
Editapplication.py:
"""Application module."""fromflaskimportFlaskfromflask_bootstrapimportBootstrapfrom.containersimportContainerfrom.importviewsdefcreate_app()->Flask:container=Container()app=Flask(__name__)app.container=containerapp.add_url_rule("/","index",views.index)bootstrap=Bootstrap()bootstrap.init_app(app)returnapp
Now we need to add the templates. For doing this we will need to add the foldertemplates/ tothegithubnavigator package. We also will need two files there:
base.html- the layoutindex.html- the main page
Createtemplates folder and put two empty files into itbase.html andindex.html:
./├──githubnavigator/│├──templates/││├──base.html││└──index.html│├──__init__.py│├──application.py│├──containers.py│└──views.py├──venv/└──requirements.txt
Now let’s fill in the layout.
Put next into thebase.html:
<!doctype html><html lang="en"> <head>{%blockhead%} <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">{%blockstyles%} <!-- Bootstrap CSS -->{{bootstrap.load_css()}}{%endblock%} <title>{%blocktitle%}{%endblock%}</title>{%endblock%} </head> <body> <!-- Your page content -->{%blockcontent%}{%endblock%}{%blockscripts%} <!-- Optional JavaScript -->{{bootstrap.load_js()}}{%endblock%} </body></html>
And put something to the index page.
Put next into theindex.html:
{%extends"base.html"%}{%blocktitle%}Github Navigator{%endblock%}{%blockcontent%}<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="{{queryifquery}}"> </div> <div class="col"> <label for="search_limit" class="col-form-label"> Limit: </label> <select class="form-control" id="search_limit" name="limit">{%forvaluein[5,10,20]%} <option{%ifvalue==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>{%forrepositoryinrepositories%}{{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%}
Ok, almost there. The last step is to makeindex view to render theindex.html template.
Editviews.py:
"""Views module."""fromflaskimportrequest,render_templatedefindex():query=request.args.get("query","Dependency Injector")limit=request.args.get("limit",10,int)repositories=[]returnrender_template("index.html",query=query,limit=limit,repositories=repositories,)
That’s it.
Make sure the app is running or useflaskrun and openhttp://127.0.0.1:5000/.
You should see:

Connect to the GitHub¶
In this section we will integrate our application with Github API.
We will usePyGithub library for working with Github API.
Let’s add it to therequirements.txt:
dependency-injectorflaskbootstrap-flaskpygithuband run in the terminal:
pipinstall-rrequirements.txt
Now we need to add Github API client the container. We will need to add two more providers fromthedependency_injector.providers module:
Factoryprovider. It will create aGithubclient.Configurationprovider. It will provide an API token and a request timeout for theGithubclient.We will specify the location of the configuration file. The configuration provider will parsethe configuration file when we create a container instance.
Editcontainers.py:
"""Containers module."""fromdependency_injectorimportcontainers,providersfromgithubimportGithubclassContainer(containers.DeclarativeContainer):config=providers.Configuration(yaml_files=["config.yml"])github_client=providers.Factory(Github,login_or_token=config.github.auth_token,timeout=config.github.request_timeout,)
Note
Don’t forget to remove the Ellipsis... from the container. We don’t need it anymoresince we container is not empty.
Now let’s add the configuration file. We will use YAML. Create an empty fileconfig.ymlin the root of the project:
./├──githubnavigator/│├──templates/││├──base.html││└──index.html│├──__init__.py│├──application.py│├──containers.py│└──views.py├──venv/├──config.yml└──requirements.txtand put next into it:
github:request_timeout:10
We will usePyYAML library for parsing the configurationfile. Let’s add it to the requirements file.
Editrequirements.txt:
dependency-injectorflaskbootstrap-flaskpygithubpyyamland install it:
pipinstall-rrequirements.txt
We will use theGITHUB_TOKEN environment variable to provide the API token. Let’s editcreate_app() to fetch the token value from it.
Editapplication.py:
"""Application module."""fromflaskimportFlaskfromflask_bootstrapimportBootstrapfrom.containersimportContainerfrom.importviewsdefcreate_app()->Flask:container=Container()container.config.github.auth_token.from_env("GITHUB_TOKEN")app=Flask(__name__)app.container=containerapp.add_url_rule("/","index",views.index)bootstrap=Bootstrap()bootstrap.init_app(app)returnapp
Now we need create an API token.
As for now, don’t worry, just take this one:
exportGITHUB_TOKEN=cbde697a6e01424856fde2b7f94a88d1b501320e
Note
To create your own token:
Follow theGithub guide.
Set the token to the environment variable:
exportGITHUB_TOKEN=<yourtoken>
That’s it.
Github API client setup is done.
Search service¶
Now it’s time to addSearchService. It will:
Perform the search.
Fetch commit extra data for each result.
Format result data.
SearchService will useGithub API client.
Create empty fileservices.py in thegithubnavigator package:
./├──githubnavigator/│├──templates/││├──base.html││└──index.html│├──__init__.py│├──application.py│├──containers.py│├──services.py│└──views.py├──venv/├──config.yml└──requirements.txtand put next into it:
"""Services module."""fromgithubimportGithubfromgithub.RepositoryimportRepositoryfromgithub.CommitimportCommitclassSearchService:"""Search service performs search on Github."""def__init__(self,github_client:Github):self._github_client=github_clientdefsearch_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)forrepositoryinrepositories[: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])ifcommitselse{},}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 addSearchService to the container.
Editcontainers.py:
"""Containers module."""fromdependency_injectorimportcontainers,providersfromgithubimportGithubfrom.importservicesclassContainer(containers.DeclarativeContainer):config=providers.Configuration(yaml_files=["config.yml"])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,)
Inject search service into view¶
Now we are ready to make the search work.
Let’s injectSearchService into theindex view. We will useWiring feature.
Editviews.py:
"""Views module."""fromflaskimportrequest,render_templatefromdependency_injector.wiringimportinject,Providefrom.servicesimportSearchServicefrom.containersimportContainer@injectdefindex(search_service:SearchService=Provide[Container.search_service]):query=request.args.get("query","Dependency Injector")limit=request.args.get("limit",10,int)repositories=search_service.search_repositories(query,limit)returnrender_template("index.html",query=query,limit=limit,repositories=repositories,)
To make the injection work we need to wire the container with theviews module.Let’s configure the container to automatically make wiring with theviews module when wecreate a container instance.
Editcontainers.py:
"""Containers module."""fromdependency_injectorimportcontainers,providersfromgithubimportGithubfrom.importservicesclassContainer(containers.DeclarativeContainer):wiring_config=containers.WiringConfiguration(modules=[".views"])config=providers.Configuration(yaml_files=["config.yml"])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,)
Make sure the app is running or useflaskrun and openhttp://127.0.0.1:5000/.
You should see:

Make some refactoring¶
Ourindex view has two hardcoded config values:
Default search query
Default results limit
Let’s make some refactoring. We will move these values to the config.
Editviews.py:
"""Views module."""fromflaskimportrequest,render_templatefromdependency_injector.wiringimportinject,Providefrom.servicesimportSearchServicefrom.containersimportContainer@injectdefindex(search_service:SearchService=Provide[Container.search_service],default_query:str=Provide[Container.config.default.query],default_limit:int=Provide[Container.config.default.limit.as_int()],):query=request.args.get("query",default_query)limit=request.args.get("limit",default_limit,int)repositories=search_service.search_repositories(query,limit)returnrender_template("index.html",query=query,limit=limit,repositories=repositories,)
Editconfig.yml:
github:request_timeout:10default:query:"DependencyInjector"limit:10
That’s it. The refactoring is done. We’ve made it cleaner.
Tests¶
In this section we will add some tests.
We will usepytest with its Flask extension andcoverage.
Editrequirements.txt:
dependency-injectorflaskbootstrap-flaskpygithubpyyamlpytest-flaskpytest-cov
And install added packages:
pipinstall-rrequirements.txt
Create empty filetests.py in thegithubnavigator package:
./├──githubnavigator/│├──templates/││├──base.html││└──index.html│├──__init__.py│├──application.py│├──containers.py│├──services.py│├──tests.py│└──views.py├──venv/├──config.yml└──requirements.txtand put next into it:
"""Tests module."""fromunittestimportmockimportpytestfromgithubimportGithubfromflaskimporturl_forfrom.applicationimportcreate_app@pytest.fixturedefapp():app=create_app()yieldappapp.container.unwire()deftest_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()]),),]withapp.container.github_client.override(github_client_mock):response=client.get(url_for("index"))assertresponse.status_code==200assertb"Results found: 2"inresponse.dataassertb"repo1-url"inresponse.dataassertb"repo1-name"inresponse.dataassertb"owner1-login"inresponse.dataassertb"owner1-url"inresponse.dataassertb"owner1-avatar-url"inresponse.dataassertb"repo2-url"inresponse.dataassertb"repo2-name"inresponse.dataassertb"owner2-login"inresponse.dataassertb"owner2-url"inresponse.dataassertb"owner2-avatar-url"inresponse.datadeftest_index_no_results(client,app):github_client_mock=mock.Mock(spec=Github)github_client_mock.search_repositories.return_value=[]withapp.container.github_client.override(github_client_mock):response=client.get(url_for("index"))assertresponse.status_code==200assertb"Results found: 0"inresponse.data
Now let’s run it and check the coverage:
py.testgithubnavigator/tests.py--cov=githubnavigatorYou should see:
platformdarwin--Python3.10.0,pytest-6.2.5,py-1.10.0,pluggy-1.0.0plugins:cov-3.0.0,flask-1.2.0collected2itemsgithubnavigator/tests.py..[100%]----------coverage:platformdarwin,python3.10.0-final-0----------NameStmtsMissCover----------------------------------------------------githubnavigator/__init__.py00100%githubnavigator/application.py130100%githubnavigator/containers.py80100%githubnavigator/services.py140100%githubnavigator/tests.py340100%githubnavigator/views.py100100%----------------------------------------------------TOTAL790100%
Note
Take a look at the highlights in thetests.py.
It emphasizes the overriding of theGithub API client.
Conclusion¶
In this tutorial we’ve built aFlask application following the dependency injection principle.We’ve used theDependencyInjector as a dependency injection framework.
Containers andProviders helped to specify how to assemble search service andintegrate it with a 3rd-party library.
Configuration provider helped to deal with reading YAML file and environment variable.
We usedWiring feature to inject the dependencies into theindex() view.Provider overriding feature helped in testing.
We kept all the dependencies injected explicitly. This will help when you need to add orchange something in future.
You can find complete project on theGithub.
What’s next?
Sponsor the project on GitHub: |