Include new-project and python

This commit is contained in:
Fred Pauchet 2023-12-07 22:19:03 +01:00
parent 3e1da2d799
commit e583606146
6 changed files with 2336 additions and 4 deletions

View File

@ -313,6 +313,33 @@ Il est (relativement) léger, et permet en plus de générer le code associé à
image::environment/insomnia.png[align="center"]
=== Makefile
Pour gagner un peu de temps, nhésitez pas à créer un fichier `+Makefile+` que vous placerez à la racine du projet. Lexemple
ci-dessous permettra, grâce à la commande `+make coverage+`, darriver au même résultat que ci-dessus:
....
# Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
....
Pour la petite histoire, `+make+` peu sembler un peu désuet, mais reste
extrêmement efficace.
=== Conclusions
Pour résumer :

View File

@ -0,0 +1,873 @@
== Démarrer un nouveau projet
Django fonctionne sur un
https://docs.djangoproject.com/en/dev/internals/release-process/[roulement
de trois versions mineures pour une version majeure], clôturé par une
version LTS (_Long Term Support_).
=== Gestion des dépendances
Comme nous en avons déjà discuté, Poetry est la solution que nous avons
choisie pour la gestion de nos dépendances. Pour installer une nouvelle
librairie, vous pouvez simplement passer par la commande
`+pip install <my_awesome_library>+`. Dans le cas de Django, et après
avoir activé lenvironnement, nous pouvons à présent y installer Django.
Comme expliqué ci-dessus, la librairie restera indépendante du reste du
système, et ne polluera aucun autre projet. nous exécuterons donc la
commande suivante:
....
$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-
env/Scrips/activate.bat pour Windows.
$ pip install django
Collecting django
Downloading Django-3.1.4
100% |################################|
Installing collected packages: django
Successfully installed django-3.1.4
....
Ici, la commande `+pip install django+` récupère la *dernière version
connue disponible dans les dépôts https://pypi.org/* (sauf si vous en
avez définis dautres. Mais cest hors sujet). Nous en avons déjà
discuté : il est important de bien spécifier la version que vous
souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de
bord.
Linstallation de Django a ajouté un nouvel exécutable:
`+django-admin+`, que lon peut utiliser pour créer notre nouvel espace
de travail. Par la suite, nous utiliserons `+manage.py+`, qui constitue
un *wrapper* autour de `+django-admin+`.
Pour démarrer notre projet, nous lançons
`+django-admin startproject gwift+`:
....
$ django-admin startproject gwift
....
Cette action a pour effet de créer un nouveau dossier `+gwift+`, dans
lequel nous trouvons la structure suivante :
....
$ tree gwift
gwift
-- gwift
----- asgi.py
----- __init__.py
----- settings.py
----- urls.py
----- wsgi.py
-- manage.py
....
Cest dans ce répertoire que vont vivre tous les fichiers liés au
projet. Le but est de faire en sorte que toutes les opérations
(maintenance, déploiement, écriture, tests, …) puissent se faire à
partir dun seul point dentrée.
Lutilité de ces fichiers est définie ci-dessous:
* `+settings.py+` contient tous les paramètres globaux à notre projet.
* `+urls.py+` contient les variables de routes, les adresses utilisées
et les fonctions vers lesquelles elles pointent.
* `+manage.py+`, pour toutes les commandes de gestion.
* `+asgi.py+` contient la définition de linterface
https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface[ASGI],
le protocole pour la passerelle asynchrone entre votre application et le
serveur Web.
* `+wsgi.py+` contient la définition de linterface
https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface[WSGI], qui
permettra à votre serveur Web (Nginx, Apache, …) de faire un pont vers
votre projet.
Indiquer quil est possible davoir plusieurs structures de dossiers et
quil ny a pas de "magie" derrière toutes ces commandes. La seule
condition est que les chemins référencés soient cohérents par rapport à
la structure sous-jacente.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel
nous stockerons les dépendances et un fichier README:
TODO
Comme nous venons dajouter une dépendance à notre projet, profitons-en
pour créer un fichier reprenant tous les dépendances de notre projet.
Celles-ci sont normalement placées dans un fichier `+requirements.txt+`.
Dans un premier temps, ce fichier peut être placé directement à la
racine du projet, mais on préférera rapidement le déplacer dans un
sous-répertoire spécifique (`+requirements+`), afin de grouper les
dépendances en fonction de leur environnement de destination:
* `+base.txt+`
* `+dev.txt+`
* `+production.txt+`
Au début de chaque fichier, il suffit dajouter la ligne
`+-r base.txt+`, puis de lancer linstallation grâce à un
`+pip install -r <nom du fichier>+`. De cette manière, il est tout à
fait acceptable de ninstaller `+flake8+` et `+django-debug-toolbar+`
quen développement par exemple. Dans limmédiat, nous allons ajouter
`+django+` dans une version égale à la version 3.2 dans le fichier
`+requirements/base.txt+`.
....
$ echo 'django==3.2' > requirements/base.txt
$ echo '-r base.txt' > requirements/prod.txt
$ echo '-r base.txt' > requirements/dev.txt
....
Une bonne pratique consiste à également placer un fichier
`+requirements.txt+` à la racine du projet, et dans lequel nous
retrouverons le contenu `+-r requirements/production.txt+` (notamment
pour Heroku).
Prenez directement lhabitude de spécifier la version ou les versions
compatibles: les librairies que vous utilisez comme dépendances
évoluent, de la même manière que vos projets. Pour être sûr et certain
le code que vous avez écrit continue à fonctionner, spécifiez la version
de chaque librairie de dépendances. Entre deux versions dune même
librairie, des fonctions sont cassées, certaines signatures sont
modifiées, des comportements sont altérés, etc. Il suffit de
parcourirles pages de _Changements incompatibles avec les anciennes
versions dans Django_
https://docs.djangoproject.com/fr/3.1/releases/3.0/[(par exemple ici
pour le passage de la 3.0 à la 3.1)] pour réaliser que certaines
opérations ne sont pas anodines, et que sans filet de sécurité, cest le
mur assuré. Avec les mécanismes dintégration continue et de tests
unitaires, nous verrons plus loin comment se prémunir dun changement
inattendu.
image:images/django-support-lts.png[image]
La version utilisée sera une bonne indication à prendre en considération
pour nos dépendances, puisquen visant une version particulière, nous ne
devrons pratiquement pas nous soucier (bon, un peu quand même, mais nous
le verrons plus tard…) des dépendances à installer, pour peu que lon
reste sous un certain seuil.
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de
nous assurer une certaine sérénité desprit (= dont nous ne occuperons
pas pendant les 3 prochaines années).
=== Django
Comme nous lavons vu ci-dessus, `+django-admin+` permet de créer un
nouveau projet. Nous faisons ici une distinction entre un *projet* et
une *application*:
* *Un projet* représente lensemble des applications, paramètres,
middlewares, dépendances, …, qui font que votre code fait ce quil est
sensé faire. Il sagit grosso modo dun câblage de tous les composants
entre eux.
* *Une application* est un contexte dexécution (vues, comportements,
pages HTML, …), idéalement autonome, dune partie du projet. Une
application est supposée avoir une portée de réutilisation, même sil ne
sera pas toujours possible de viser une généricité parfaite.
Pour `+gwift+`, nous aurons :
.Projet Django vs Applications
image::images/django/django-project-vs-apps-gwift.png[images/django/django-project-vs-apps-gwift]
. Une première application pour la gestion des listes de souhaits et des
éléments,
. Une deuxième application pour la gestion des utilisateurs,
. Voire une troisième application qui gérera les partages entre
utilisateurs et listes.
Nous voyons également que la gestion des listes de souhaits et éléments
aura besoin de la gestion des utilisateurs - elle nest pas autonome -,
tandis que la gestion des utilisateurs na aucune autre dépendance
quelle-même.
Pour `+khana+`, nous pourrions avoir quelque chose comme ceci:
.Django Project vs Applications
image::images/django/django-project-vs-apps-khana.png[images/django/django-project-vs-apps-khana]
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la
gestion des utilisateurs et la possibilité quils auront de communiquer
entre eux. Ceci pourrait être commun aux deux projets. Nous pouvons
clairement visualiser le principe de *contexte* pour une application:
celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage
et pourrait ainsi être réutilisée dans un autre projet. Cest en ça que
consistent les https://www.djangopackages.com/[paquets Django] déjà
disponibles: ce sont "_simplement_" de petites applications empaquetées
et pouvant être réutilisées dans différents contextes (eg.
https://github.com/tomchristie/django-rest-framework[Django-Rest-Framework],
https://github.com/django-debug-toolbar/django-debug-toolbar[Django-Debug-Toolbar],
Le projet soccupe principalement dappliquer une couche de glue entre
différentes applications. Découper proprement un projet en plusieurs
applications totalement autonomes est illusoire. Une bonne pratique
consiste à rester pragmatique et à partir avec *une seule* application,
et la découper lorsque vous jugerez quelle grossit trop ou trop
rapidement : découper trop rapidement et sans raison valable une
application en plein de petits fichiers va gâcher énormément de temps de
développement, sans apporter de réels bénéfices. Dautre part, une
(autre) bonne pratique consiste à aussi *limiter à cinq* le nombre de
modèles différents dans chaque application. Tant que ce seuil ne sera
pas atteint, laissez ce principe de côté.
==== manage.py
Le fichier `+manage.py+` que vous trouvez à la racine de votre projet
est un *wrapper* sur les commandes `+django-admin+`. A partir de
maintenant, nous nutiliserons plus que celui-là pour tout ce qui
touchera à la gestion de notre projet :
* `+manage.py check+` pour vérifier (en surface…) que votre projet ne
rencontre aucune erreur évidente
* `+manage.py check --deploy+`, pour vérifier (en surface aussi) que
lapplication est prête pour un déploiement
* `+manage.py runserver+` pour lancer un serveur de développement
* `+manage.py test+` pour découvrir les tests unitaires disponibles et
les lancer.
La liste complète peut être affichée avec `+manage.py help+`. Vous
remarquerez que ces commandes sont groupées selon différentes
catégories:
* *auth*: création dun nouveau super-utilisateur, changer le mot de
passe pour un utilisateur existant.
* *django*: vérifier la *conformité* du projet, lancer un *shell*,
*dumper* les données de la base, effectuer une migration du schéma, … Ce
sont des commandes dadministration générale.
* *sessions*: suppressions des sessions en cours
* *staticfiles*: gestion des fichiers statiques et lancement du serveur
de développement.
Chaque section correspond à une application. En analysant le code, ces
applications peuvent être trouvées sous `+django.contrib+`.
Nous verrons plus tard comment ajouter de nouvelles commandes.
Si nous démarrons la commande `+python manage.py runserver+`, nous
verrons la sortie console suivante:
....
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
[...]
December 15, 2020 - 20:45:07
Django version 3.1.4, using settings 'gwift.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
....
Si nous nous rendons sur la page http://127.0.0.1:8000 (ou
http://localhost:8000) comme le propose si gentiment notre (nouveau)
meilleur ami, nous verrons ceci:
.python manage.py runserver (Non, ce nest pas Challenger)
image::images/django/manage-runserver.png[images/django/manage-runserver]
Nous avons mis un morceau de la sortie console entre crochet `+[+``+…+`
ci-dessus, car elle concerne les migrations. Si vous avez suivi les
étapes jusquici, vous avez également dû voir un message type
`+You have 18 unapplied migration(s). +``+[+``+… Run python manage.py migrate to apply them.+`
Cela concerne les migrations, et cest un point que nous verrons un peu
plus tard.
=== Minimal Viable Application
Maintenant que nous avons a vu à quoi servait `+manage.py+`, nous
pouvons créer notre nouvelle application grâce à la commande
`+manage.py startapp <label>+`.
Notre première application servira à structurer des listes de souhaits,
les éléments qui les composent et les pourcentages de participation que
chaque utilisateur aura souhaité offrir. De manière générale, essayez de
trouver un nom éloquent, court et qui résume bien ce que fait
lapplication. Pour nous, ce sera donc `+wish+`.
....
$ python manage.py startapp wish
....
Résultat? Django nous a créé un répertoire `+wish+`, dans lequel nous
trouvons les fichiers et dossiers suivants:
* `+wish/__init__.py+` pour que notre répertoire `+wish+` soit converti
en package Python.
* `+wish/admin.py+` servira à structurer ladministration de notre
application. Chaque information peut être gérée facilement au travers
dune interface générée à la volée par le framework. Nous y reviendrons
par la suite.
* `+wish/apps.py+` qui contient la configuration de lapplication et qui
permet notamment de fixer un nom ou un libellé
https://docs.djangoproject.com/en/stable/ref/applications/
* `+wish/migrations/+` est le dossier dans lequel seront stockées toutes
les différentes migrations de notre application (= toutes les
modifications que nous apporterons aux données que nous souhaiterons
manipuler)
* `+wish/models.py+` représentera et structurera nos données. Ce modèle
est intimement lié aux migrations.
* `+wish/tests.py+` pour les tests unitaires.
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire
`+wish+` dans votre répertoire `+gwift+` existant. Cest _une_ forme de
convention.
La structure de vos répertoires devient celle-ci:
TODO
Notre application a bien été créée, et nous lavons déplacée dans le
répertoire `+gwift+` ! footnote:[Il manque quelques fichiers utiles, qui
seront décrits par la suite, pour quune application soit réellement
autonome: templates, `+urls.py+`, managers, services, …]
=== Fonctionnement général
Le métier de programmeur est devenu de plus en plus complexe. Il y a 20
ans, nous pouvions nous contenter dune simple page PHP dans laquelle
nous mixions lensemble des actions à réaliser : requêtes en bases de
données, construction de la page, … La recherche dune solution à un
problème nétait pas spécialement plus complexe - dans la mesure où le
rendu des enregistrements en direct nétait finalement quune forme un
chouia plus évoluée du `+print()+` ou des `+System.out.println()+` -
mais cétait lévolutivité des applications qui en prenait un coup: une
grosse partie des tâches étaient dupliquées entre les différentes pages,
et lajout dune nouvelle fonctionnalité était relativement ardue.
Django (et dautres frameworks) résolvent ce problème en se basant
ouvertement sur le principe de `+Dont repeat yourself+` footnote:[DRY].
Chaque morceau de code ne doit apparaitre quune seule fois, afin de
limiter au maximum la redite (et donc, lapplication dun même correctif
à différents endroits).
Le chemin parcouru par une requête est expliqué en (petits) détails
ci-dessous.
*Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et
servie par notre application*. Ici, nous prenons lexemple de lURL
fictive `+https://gwift/wishes/91827+`. Lorsque cette URL "arrive" dans
notre application, son point dentrée se trouvera au niveau des fichiers
`+asgi.py+` ou `+wsgi.py+`. Nous verrons cette partie plus tard, et nous
pouvons nous concentrer sur le chemin interne quelle va parcourir.
.How it works
image::images/diagrams/django-how-it-works.png[images/diagrams/django-how-it-works]
*Etape 0* - La première étape consiste à vérifier que cette URL répond à
un schéma que nous avons défini dans le fichier `+gwift/urls.py+`.
[source,Python]
----
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details),
]
----
*Etape 1* - Si ce nest pas le cas, lapplication nira pas plus loin et
retournera une erreur à lutilisateur.
*Etape 2* - Django va parcourir lensemble des _patterns_ présents dans
le fichier `+urls.py+` et sarrêtera sur le premier qui correspondra à
la requête quil a reçue. Ce cas est relativement trivial: la requête
`+/wishes/91827+` a une correspondance au niveau de la ligne
`+path("wishes/<int:wish_id>+` dans lexemple ci-dessous. Django va
alors appeler la fonction footnote:[Qui ne sera pas toujours une
fonction. Django sattend à trouver un _callable_, cest-à-dire
nimporte quel élément quil peut appeler comme une fonction.] associée
à ce _pattern_, cest-à-dire `+wish_details+` du module `+gwift.views+`.
* Nous importons la fonction `+wish_details+` du module `+gwift.views+`
* Champomy et cotillons! Nous avons une correspondance avec
`+wishes/details/91827+`
Le module `+gwift.views+` qui se trouve dans le fichier
`+gwift/views.py+` peut ressembler à ceci:
[source,Python]
----
# gwift/views.py
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"generated_at": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
----
Pour résumer, cette fonction permet:
. De construire un _contexte_, qui est représenté sous la forme dun
dictionnaire associant des clés à des valeurs. Les clés sont
respectivement `+user_name+`, `+user_first_name+` et `+now+`, tandis que
leurs valeurs respectives sont `+Bond+`, `+James+` et le
`+moment présent+` footnote:[Non, pas celui dEckhart Tolle].
. Nous passons ensuite ce dictionnaire à un canevas,
`+wish_details.html+`, que lon trouve normalement dans le répertoire
`+templates+` de notre projet, ou dans le répertoire `+templates+`
propre à notre application.
. Lapplication du contexte sur le canevas au travers de la fonction
`+render+` nous donne un résultat formaté.
[source,html]
----
<!-- fichier wish_details.html -->
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
<p>This page was generated at {{ generated_at }}</p>
</body>
</html>
----
Après application de notre contexte sur ce template, nous obtiendrons ce
document, qui sera renvoyé au navigateur de lutilisateur qui aura fait
la requête initiale:
[source,html]
----
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is Bond. James Bond.</p>
<p>This page was generated at 2027-03-19 19:47:38</p>
</body>
</html>
----
.Résultat
image::images/django/django-first-template.png[images/django/django-first-template]
=== Configuration globale
==== Structure finale
En repartant de la structure initiale décrite au chapitre précédent,
nous arrivons à ceci.
TODO : passer à poetry
==== Cookie-cutter
Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un
nouveau projet, non? Sachant quen plus, nous avons dû modifier des
fichiers, déplacer des dossiers, ajouter des dépendances, configurer une
base de données, …
Bonne nouvelle! Il existe des générateurs, permettant de démarrer
rapidement un nouveau projet sans (trop) se prendre la tête. Le plus
connu (et le plus personnalisable) est
https://cookiecutter.readthedocs.io/[Cookie-Cutter], qui se base sur des
canevas _type https://pypi.org/project/Jinja2/[Jinja2]_, pour créer une
arborescence de dossiers et fichiers conformes à votre manière de
travailler. Et si vous avez la flemme de créer votre propre canevas,
vous pouvez utiliser https://cookiecutter-django.readthedocs.io[ceux qui
existent déjà].
Pour démarrer, créez un environnement virtuel (comme dhabitude):
....
python -m venv .venvs\cookie-cutter-khana
.venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) $ pip install cookiecutter
Collecting cookiecutter
[...]
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 arrow-0.17.0
binaryornot-0.4.4 certifi-2020.12.5 chardet-4.0.0 click-7.1.2 cookiecutter-
1.7.2 idna-2.10 jinja2-time-0.2.0 poyo-0.5.0 python-dateutil-2.8.1 python-
slugify-4.0.1 requests-2.25.1 six-1.15.0 text-unidecode-1.3 urllib3-1.26.2
(cookie-cutter-khana) $ cookiecutter https://github.com/pydanny/cookiecutter-
django
[...]
[SUCCESS]: Project initialized, keep up the good work!
....
Si vous explorez les différents fichiers, vous trouverez beaucoup de
similitudes avec la configuration que nous vous proposions ci-dessus. En
fonction de votre expérience, vous serez tenté de modifier certains
paramètres, pour faire correspondre ces sources avec votre utilisation
ou vos habitudes.
Il est aussi possible dutiliser largument `+--template+`, suivie dun
argument reprenant le nom de votre projet (`+<my_project>+`), lors de
linitialisation dun projet avec la commande `+startproject+` de
`+django-admin+`, afin de calquer votre arborescence sur un projet
existant. La
https://docs.djangoproject.com/en/stable/ref/django-admin/#startproject[documentation]
à ce sujet est assez complète.
....
django-admin.py startproject --template=https://[...].zip <my_project>
....
=== Tests unitaires
Il y a deux manières décrire les tests: soit avant, soit après
limplémentation. Oui, idéalement, les tests doivent être écrits à
lavance. Entre nous, on ne va pas râler si vous faites linverse,
limportant étant que vous le fassiez. Une bonne métrique pour vérifier
lavancement des tests est la couverture de code.
Chaque application est créée par défaut avec un fichier *tests.py*, qui
inclut la classe `+TestCase+` depuis le package `+django.test+`:
On a deux choix ici:
. Utiliser les librairies de test de Django
. Utiliser Pytest
==== django.test
[source,Python]
----
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
----
==== Pytest
==== Couverture de code
Quel que soit le framework de tests choisi (django-tests, pytest,
unittest, …), la couverture de code est une analyse qui donne un
pourcentage lié à la quantité de code couvert par les tests. Il ne
sagit pas de vérifier que le code est bien testé, mais de vérifier
quelle partie du code est testée. Le paquet coverage se charge dévaluer
le pourcentage de code couvert par les tests. Avec pytest, il convient
dutiliser le paquet pytest-cov, suivi de la commande pytest
`+cov=gwift tests/+`. Si vous préférez rester avec le cadre de tests de
Django, vous pouvez passer par le paquet django-coverage-plugin.
Ajoutez-le dans le fichier requirements/base.txt, et lancez une
couverture de code grâce à la commande coverage. La configuration peut
se faire dans un fichier .coveragerc que vous placerez à la racine de
votre projet, et qui sera lu lors de lexécution.
....
# requirements/base.text
[...]
django_coverage_plugin
....
....
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
....
Nous pouvons à présent jouer au jeu de la couverture, qui consiste à
augmenter ou égaliser la couverture existante à chaque nouvelle
fonctionnalité ajoutée ou bug corrigé. De cette manière, sans arriver à
une couverture de 100%, chaque modification du code améliorera la base
existante. footnote:[cf. Two Scoops of Django]. Suivant loutil
dintégration continue que vous utiliserez, cette évolution pourra être
affichée à chaque demande de fusion, et pourra être considérée comme un
indicateur de qualité.
....
$ coverage run --source "." manage.py test
$ coverage report
Name Stmts Miss Cover
---------------------------------------------
gwift\gwift\__init__.py 0 0 100%
gwift\gwift\settings.py 17 0 100%
gwift\gwift\urls.py 5 5 0%
gwift\gwift\wsgi.py 4 4 0%
gwift\manage.py 6 0 100%
gwift\wish\__init__.py 0 0 100%
gwift\wish\admin.py 1 0 100%
gwift\wish\models.py 49 16 67%
gwift\wish\tests.py 1 1 0%
gwift\wish\views.py 6 6 0%
---------------------------------------------
TOTAL 89 32 64%
----
$ coverage html
....
==== Recommandations sur les tests
En résumé, il est recommandé de:
. Tester que le nommage dune URL (son attribut `+name+` dans les
fichiers `+urls.py+`) corresponde à la fonction que lon y a définie
. Tester que lURL envoie bien vers lexécution dune fonction (et que
cette fonction est celle que lon attend)
===== Tests de nommage
[source,python]
----
from django.core.urlresolvers import reverse
from django.test import TestCase
class HomeTests(TestCase):
def test_home_view_status_code(self):
url = reverse("home")
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
----
===== Tests dURLs
[source,python]
----
from django.core.urlresolvers import reverse
from django.test import TestCase
from .views import home
class HomeTests(TestCase):
def test_home_view_status_code(self):
view = resolve("/")
self.assertEquals(view.func, home)
----
==== Couverture de code
Pour lexemple, nous allons écrire la fonction
`+percentage_of_completion+` sur la classe `+Wish+`, et nous allons
spécifier les résultats attendus avant même dimplémenter son contenu.
Prenons le cas où nous écrivons la méthode avant son test:
[source,python]
----
class Wish(models.Model):
[...]
@property
def percentage_of_completion(self):
"""
Calcule le pourcentage de complétion pour un élément.
"""
number_of_linked_parts = WishPart.objects.filter(wish=self).count()
total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total)
return percentage * 100
----
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
....
$ coverage run --source "." src/manage.py test wish
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%
....
Si vous générez le rapport HTML avec la commande `+coverage html+` et
que vous ouvrez le fichier
`+coverage_html_report/src_wish_models_py.html+`, vous verrez que les
méthodes en rouge ne sont pas testées. *A contrario*, la couverture de
code atteignait *98%* avant lajout de cette nouvelle méthode.
Pour cela, on va utiliser un fichier `+tests.py+` dans notre application
`+wish+`. *A priori*, ce fichier est créé automatiquement lorsque vous
initialisez une nouvelle application.
[source,python]
----
from django.test import TestCase
class TestWishModel(TestCase):
def test_percentage_of_completion(self):
"""
Vérifie que le pourcentage de complétion d'un souhait
est correctement calculé.
Sur base d'un souhait, on crée quatre parts et on vérifie
que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%.
"""
wishlist = Wishlist(
name='Fake WishList',
description='This is a faked wishlist'
)
wishlist.save()
wish = Wish(
wishlist=wishlist,
name='Fake Wish',
description='This is a faked wish',
number_of_parts=4
)
wish.save()
part1 = WishPart(wish=wish, comment='part1')
part1.save()
self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2')
part2.save()
self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3')
part3.save()
self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4')
part4.save()
self.assertEqual(100, wish.percentage_of_completion)
----
Lattribut `+@property+` sur la méthode `+percentage_of_completion()+`
va nous permettre dappeler directement la méthode
`+percentage_of_completion()+` comme sil sagissait dune propriété de
la classe, au même titre que les champs `+number_of_parts+` ou
`+numbers_available+`. Attention que ce type de méthode contactera la
base de données à chaque fois quelle sera appelée. Il convient de ne
pas surcharger ces méthodes de connexions à la base: sur de petites
applications, ce type de comportement a très peu dimpacts, mais ce
nest plus le cas sur de grosses applications ou sur des méthodes
fréquemment appelées. Il convient alors de passer par un mécanisme de
*cache*, que nous aborderons plus loin.
En relançant la couverture de code, on voit à présent que nous arrivons
à 99%:
....
$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%
....
En continuant de cette manière (ie. Ecriture du code et des tests,
vérification de la couverture de code), on se fixe un objectif idéal dès
le début du projet. En prenant un développement en cours de route,
fixez-vous comme objectif de ne jamais faire baisser la couverture de
code.
A noter que tester le modèle en lui-même (ses attributs ou champs) ou
des composants internes à Django na pas de sens: cela reviendrait à
mettre en doute son fonctionnement interne. Selon le principe du SRP
link:#SRP[[SRP]], cest le framework lui-même qui doit en assurer la
maintenance et le bon fonctionnement.
=== Licence
Choisissez une licence. Si votre projet nen a pas, vous pourriez être
tenu responsable de manquements ou de bugs collatéraux. En cas de
désastre médical ou financier, ce simple fichier peut faire toute la
différence.
_React, for example, has an additional clause that could potentially
cause patent claim conflicts with React users_ . Cette issue a été
adressée en 2017
footnote:[hhttps://github.com/facebook/react/issues/7293].
Un autre exemple concerne StackOverflow, qui utilisait une licence
Creative Commons de type CC-BY-SA pour chaque contenu posté sur sa
plateforme. Cette licence est cependante limitante, dans la mesure où
elle obligeait que chaque utilisateur cite lorigine du code utilisé.
Ceci nétait pas vraiment connu de tous, mais si un utilisateur qui
venait à opérer selon des contraintes relativement strictes (en milieu
professionnel, par exemple) venait à poser une question sur la
plateforme, il aurait été légalement obligé de réattribuer la réponse
quil aurait pu utiliser. StackOverflow est ainsi passé vers une licence
MIT présentant moins de restrictions.
Trois licences footnote:[Bien quil en existe beaucoup] sont
généralement proposées et utilisées:
. *MIT*
. *GPLv3*
. *Fair Source*, annoncée en 2015, qui propose une solution à la
nécessité de proposer une licence gratuite pour une utilisation
personnelle ou en petites entreprises, tout en étant payante pour une
une utilisation commerciale plus large. footnote:[_Under Fair Source,
code is free to view, download, execute, and modify up to a certain
number of users in an organization. After that limit is reached, the
organization must pay a licencing fee, determined by the published -
https://fair.io_]
. *WTFPL*
Mike Perham, qui maintient Sidekiq, a ainsi proposé une forme de dualité
entre la mise à disposition du code source et son utilisation :
____
_Remember: Open Source is not Free Software. The source may be viewable
on GitHub but that doesnt mean anyone can use it for any purpose.
Theres no reason you cant make your source code accessible but also
charge to use it. As long as you are the owner of the code, you have the
right to licence it however you want._
_...[The] reality is most smaller OSS project have a single person doing
95% of the work. If this is true, be grateful for unpaid help but dont
feel guilty about keeping 100% of the income._
____
=== Conclusions
Comme nous lavons vu dans la première partie, Django est un framework
complet, intégrant tous les mécanismes nécessaires à la bonne évolution
dune application. Il est possible de démarrer petit, et de suivre
lévolution des besoins en fonction de la charge estimée ou ressentie,
dajouter un mécanisme de mise en cache, des logiciels de suivi, …

1359
book/environment/python.adoc Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,3 +5,72 @@
Lorsque nous démarrons une nouvelle application, la toute première version est généralement la partie la plus simple à mettre en place : elle ne nécessite pratiquement aucune maintenance ou considération pour les futurs désidérata de vos clients.
Les tests unitaires et d'intégration *vous* place comme client et consommateur de votre propre travail.
=== Tests unitaires
Comme tout bon *langage de programmation moderne* qui se respecte,
Python embarque tout un environnement facilitant le lancement de tests;
Une bonne pratique (parfois discutée) consiste cependant à switcher vers
`+pytest+`, qui présente quelques avantages par rapport au module
`+unittest+`:
* Une syntaxe plus concise (au prix de
https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html[quelques
conventions], même si elles restent configurables): un test est une
fonction, et ne doit pas obligatoirement faire partie dune classe
héritant de `+TestCase+` - la seule nécessité étant que cette fonction
fasse partie dun module commençant ou finissant par "test"
(`+test_example.py+` ou `+example_test.py+`).
* Une compatibilité avec du code Python "classique" - vous ne devrez
donc retenir quun seul ensemble de commandes ;-)
* Des _fixtures_ faciles à réutiliser entre vos différents composants
* Une compatibilité avec le reste de lécosystème, dont la couverture de
code présentée ci-dessous.
Ainsi, après installation, il nous suffit de créer notre module
`+test_models.py+`, dans lequel nous allons simplement tester laddition
dun nombre et dune chaîne de caractères (oui, cest complètement
biesse; on est sur la partie théorique ici):
[source,Python]
----
def test_add():
assert 0 + 0 == "La tête à Toto"
----
Forcément, cela va planter. Pour nous en assurer (dès fois que quelquun
en doute), il nous suffit de démarrer la commande `+pytest+`:
....
pytest
================= test session starts =================
platform ...
rootdir: ...
plugins: django-4.1.0
collected 1 item
gwift\test_models.py F
[100%]
================= FAILURES =================
_________________ test_basic_add _________________
def test_basic_add():
> assert 0 + 0 == "La tête à Toto"
E AssertionError: assert (0 + 0) == 'La tête à Toto'
tests.py:2: AssertionError
================= short test summary info =================
FAILED tests.py::test_basic_add - AssertionError: assert (0 + 0) == 'La tête à Toto'
================= 1 failed in 0.10s =================
....
==== Couverture de code
Avec `+pytest+`, il convient dutiliser le paquet
https://pypi.org/project/pytest-cov/[`+pytest-cov+`], suivi de la
commande `+pytest --cov=gwift tests/+`.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez
passer par le
paquethttps://pypi.org/project/django-coverage-plugin/[django-coverage-plugin].
Ajoutez-le dans le fichier `+requirements/base.txt+`, et lancez une
couverture de code grâce à la commande `+coverage+`. La configuration
peut se faire dans un fichier `+.coveragerc+` que vous placerez à la
racine de votre projet, et qui sera lu lors de lexécution.

View File

@ -51,8 +51,12 @@ include::book/environment/maintenability.adoc[]
include::book/environment/elements-of-architecture.adoc[]
include::book/environment/tests.adoc[]
include::book/environment/development-tools.adoc[]
include::book/environment/python.adoc[]
include::book/environment/tests.adoc[]
include::book/environment/gdpr.adoc[]
include::book/environment/new-project.adoc[]

View File

@ -1,6 +1,6 @@
= Reporting
== Reporting
== Status page
=== Status page
https://www.atlassian.com/software/statuspage/pricing