je suis le créateur de Dependency Injector . Il s'agit d'un framework d'injection de dépendances pour Python.
Dans ce didacticiel, je souhaite montrer comment utiliser l'injecteur de dépendances pour développer des applications Flask.
Le manuel comprend les parties suivantes:
- Qu'allons-nous construire?
- Préparez l'environnement
- Structure du projet
- Bonjour le monde!
- Y compris les styles
- Connecter Github
- Service de recherche
- Connecter la recherche
- Un peu de refactoring
- Ajouter des tests
- Conclusion
Le projet terminé peut être trouvé sur Github .
Pour commencer, vous devez avoir:
- Python 3.5+
- Environnement virtuel
Et il est souhaitable d'avoir:
- Compétences de développement initial avec Flask
- Comprendre le principe de l'injection de dépendances
Qu'allons-nous construire?
Nous allons créer une application qui vous aide à rechercher des référentiels sur Github. Appelons cela Github Navigator.
Comment fonctionne Github Navigator?
- L'utilisateur ouvre une page Web sur laquelle il est invité à entrer une requête de recherche.
- L'utilisateur entre une requête et appuie sur Entrée.
- Le navigateur Github recherche les référentiels correspondants sur Github.
- Une fois la recherche terminée, le navigateur Github montre à l'utilisateur une page Web avec des résultats.
- La page de résultats affiche tous les référentiels trouvés et la requête de recherche.
- Pour chaque référentiel, l'utilisateur voit:
- nom du référentiel
- propriétaire du référentiel
- le dernier commit dans le référentiel
- L'utilisateur peut cliquer sur n'importe lequel des éléments pour ouvrir sa page sur Github.
Préparez l'environnement
Tout d'abord, nous devons créer un dossier de projet et un environnement virtuel:
mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv
Maintenant, activons l'environnement virtuel:
. venv/bin/activate
L'environnement est prêt, commençons maintenant par la structure du projet.
Structure du projet
Créons la structure suivante dans le dossier actuel. Laissez tous les fichiers vides pour le moment. Ce n'est pas encore critique.
Structure initiale:
./
├── githubnavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Il est temps d'installer Flask et Dependency Injector.
Ajoutons les lignes suivantes au fichier
requirements.txt
:
dependency-injector
flask
Maintenant, installons-les:
pip install -r requirements.txt
Et vérifiez que l'installation a réussi:
python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"
Vous verrez quelque chose comme:
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2
Bonjour le monde!
Créons une application hello world minimale.
Ajoutons les lignes suivantes au fichier
views.py
:
"""Views module."""
def index():
return 'Hello, World!'
Ajoutons maintenant un conteneur pour les dépendances (ci-après juste un conteneur). Le conteneur contiendra tous les composants de l'application. Ajoutons les deux premiers composants. Ceci est une application et une vue Flask
index
.
Ajoutons ce qui suit au fichier
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)
Nous devons maintenant créer une fabrique d'applications Flask. Il est généralement appelé
create_app()
. Cela créera un conteneur. Le conteneur sera utilisé pour créer l'application Flask. La dernière étape consiste à configurer le routage - nous attribuerons la vue index_view
du conteneur pour gérer les requêtes à la racine "/" de notre application.
Éditons
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
Le conteneur est le premier objet de l'application. Il est utilisé pour récupérer tous les autres objets.
Notre application est maintenant prĂŞte Ă dire "Hello, World!"
Exécuter dans un terminal:
export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run
La sortie devrait ressembler Ă ceci:
* 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
Ouvrez votre navigateur et accédez à http://127.0.0.1:5000/ .
Vous verrez "Hello, World!"
Excellent. Notre application minimale démarre et fonctionne avec succès.
Rendons-le un peu plus joli.
Y compris les styles
Nous utiliserons Bootstrap 4 . Utilisons l'extension Bootstrap-Flask pour cela . Cela nous aidera à ajouter tous les fichiers nécessaires en quelques clics.
Ajouter
bootstrap-flask
Ă requirements.txt
:
dependency-injector
flask
bootstrap-flask
et exécutez dans le terminal:
pip install --upgrade -r requirements.txt
Ajoutons maintenant l'extension
bootstrap-flask
au conteneur.
Modifier
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)
Initialisons l'extension
bootstrap-flask
. Nous devrons changer create_app()
.
Modifier
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
Nous devons maintenant ajouter des modèles. Pour ce faire, nous devons ajouter un dossier
templates/
au package githubnavigator
. Ajoutez deux fichiers dans le dossier des modèles:
base.html
- modèle de baseindex.html
- modèle de page principale
Créez un dossier
templates
et deux fichiers vides à l'intérieur base.html
et index.html
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
Maintenant, remplissons le modèle de base.
Ajoutons les lignes suivantes au fichier
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>
Maintenant, remplissons le modèle de page maître.
Ajoutons les lignes suivantes au fichier
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 %}
Super, presque terminé. La dernière étape consiste
index
à modifier la vue pour utiliser le modèle index.html
.
Éditons
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,
)
Terminé.
Assurez-vous que l'application est en cours d'exécution ou exécutée
flask run
et ouvrez http://127.0.0.1:5000/ .
Tu devrais voir:
Connecter Github
Dans cette section, nous intégrerons notre application à l'API Github.
Nous utiliserons la bibliothèque PyGithub .
Ajoutons-le Ă
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
et exécutez dans le terminal:
pip install --upgrade -r requirements.txt
Nous devons maintenant ajouter le client API Github au conteneur. Pour ce faire, nous devrons utiliser deux nouveaux fournisseurs du module
dependency_injector.providers
:
- Le fournisseur
Factory
créera le client Github. - Le fournisseur
Configuration
transmettra le jeton d'API et le délai d'expiration Github au client.
Faisons le.
Éditons
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)
Nous avons utilisé les paramètres de configuration avant de définir leurs valeurs. C'est le principe selon lequel le fournisseur fonctionneConfiguration
.
Nous utilisons d'abord, puis nous définissons les valeurs.
Ajoutons maintenant le fichier de configuration.
Nous utiliserons YAML.
Créez un fichier vide
config.yml
Ă la racine du projet:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
Et remplissez-le avec les lignes suivantes:
github:
request_timeout: 10
Pour travailler avec le fichier de configuration, nous utiliserons la bibliothèque PyYAML . Ajoutons-le au fichier avec les dépendances.
Modifier
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
et installez la dépendance:
pip install --upgrade -r requirements.txt
Nous utiliserons une variable d'environnement pour transmettre le jeton API
GITHUB_TOKEN
.
Maintenant, nous devons Ă©diter
create_app()
pour faire 2 actions lorsque l'application démarre:
- Charger la configuration depuis
config.yml
- Charger le jeton d'API Ă partir de la variable d'environnement
GITHUB_TOKEN
Modifier
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
Nous devons maintenant créer un jeton API.
Pour cela, vous avez besoin de:
- Suivez ce tutoriel sur Github
- DĂ©finissez le jeton sur la variable d'environnement:
export GITHUB_TOKEN=<your token>
Cet élément peut être temporairement ignoré.
L'application fonctionnera sans jeton, mais avec une bande passante limitée. Limite pour les clients non authentifiés: 60 requêtes par heure. Le jeton est nécessaire pour augmenter ce quota à 5000 par heure.
Terminé.
L'installation de l'API client Github est terminée.
Service de recherche
Il est temps d'ajouter un service de recherche
SearchService
. Il sera:
- Rechercher sur Github
- Obtenez des données supplémentaires sur les commits
- Convertir le résultat du format
SearchService
utilisera le client API Github.
Créez un fichier vide
services.py
dans le package githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
et ajoutez-y les lignes suivantes:
"""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,
}
Maintenant, ajoutons
SearchService
au conteneur.
Modifier
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)
Connecter la recherche
Nous sommes maintenant prĂŞts pour que la recherche fonctionne. Utilisons
SearchService
en index
vue.
Modifier
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,
)
Modifions maintenant le conteneur pour transmettre la dépendance
SearchService
Ă la vue index
lorsqu'elle est appelée.
Modifier
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,
)
Assurez-vous que l'application est en cours d'exécution ou exécutée
flask run
et ouvrez http://127.0.0.1:5000/ .
Tu verras:
Un peu de refactoring
Notre vue
index
contient deux valeurs codées en dur:
- Terme de recherche par défaut
- Limite du nombre de résultats
Faisons un peu de refactoring. Nous transférerons ces valeurs dans la configuration.
Modifier
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,
)
Maintenant, nous avons besoin que ces valeurs soient transmises sur appel. Mettons Ă jour le conteneur.
Modifier
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,
)
Maintenant, mettons Ă jour le fichier de configuration.
Modifier
config.yml
:
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
Terminé.
Le refactoring est terminé. Mu a rendu le code plus propre.
Ajouter des tests
Ce serait bien d'ajouter quelques tests. Faisons le.
Nous utiliserons pytest et la couverture .
Modifier
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov
et installez de nouveaux packages:
pip install -r requirements.txt
Créez un fichier vide
tests.py
dans le 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
et ajoutez-y les lignes suivantes:
"""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
Commençons maintenant les tests et vérifions la couverture:
py.test githubnavigator/tests.py --cov=githubnavigator
Tu verras:
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%
Remarquez comment nous remplaçonsgithub_client
par simulacre en utilisant la méthode.override()
. De cette façon, vous pouvez remplacer la valeur de retour de n'importe quel fournisseur.
Conclusion
Nous avons construit notre application Flask en utilisant l'injection de dépendances. Nous avons utilisé Dependency Injector comme cadre d'injection de dépendances.
La partie principale de notre application est le conteneur. Il contient tous les composants de l'application et leurs dépendances en un seul endroit. Cela permet de contrôler la structure de l'application. Il est facile de comprendre et de changer:
"""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,
)
Un conteneur comme carte de votre application. Vous savez toujours ce qui dépend de quoi.
Et après?
- En savoir plus sur Dependency Injector sur GitHub
- Consultez la documentation sur Lire les documents
- Vous avez une question ou trouvez un bug? Ouvrir un problème sur Github