Move tests, create project structure, ...

This commit is contained in:
Fred Pauchet 2024-01-31 21:00:15 +01:00
parent 1a8b37b2a6
commit df11e89a61
19 changed files with 591 additions and 634 deletions

View File

@ -45,7 +45,7 @@ $ poetry new django-gecko
$ django-gecko
├── README.md
├── django_gecko
   └── __init__.py
└── __init__.py
├── pyproject.toml
└── tests
└── __init__.py

View File

@ -1 +1,75 @@
== Différentes structures de projets
== Différentes structures de projets
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.
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire `wish` dans votre répertoire `gwift` existant. Cest _une_ forme de
convention.
Il manque quelques fichiers utiles, qui seront décrits par la suite, pour quune application soit réellement autonome: templates, `urls.py`, managers, services, ...
=== Structure d'une application
. fichier `urls.py` pour définir son propre contexte d'appel,
==== 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>
....

View File

@ -57,7 +57,7 @@ Les composants périphériques gravitent autour du fonctionnement normal de l
____
"Focusing on resilience often means that a firm can handle events that may cause crises for most organizations in a manner that is routine and mundane.
Specific architectural patterns that they implemented includedfail fasts (settings aggressive timeouts such that failing components dont make the entire system crawl to a halt), fallbacks (designing each feature to degrade or fall back to a lower quality representation), and feature removal (removing non-critical features when they run slowly from any given page to prevent them from impacting the member experience).
Another astonishing example of the resilience that the netflix team created beyond preserving business continuity during the AWS outage, was that they went over six hours into the AWS outage before declaring a Sev 1 incident, assuming that AWS service would eventually be restored (i.e., "AWS will come backit usually does, right ?").
Another astonishing example of the resilience that the netflix team created beyond preserving business continuity during the AWS outage, was that they went over six hours into the AWS outage before declaring a Sev 1 incident, assuming that AWS service would eventually be restored (i.e., "AWS will come back...it usually does, right ?").
Only after six hours into the outage did they activate any business continuity procedures.
____
@ -82,8 +82,8 @@ temps que prennent chacune dentre elles |HAProxy
Si nous schématisons linfrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante :
. Lutilisateur fait une requête via son navigateur (Firefox ou Chrome)
. Le navigateur envoie une requête http, sa version, un verbe (GET, POST, ), un port et éventuellement du contenu
. Le firewall du serveur (Debian GNU/Linux, CentOS, vérifie si la requête peut être prise en compte
. Le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. Le firewall du serveur (Debian GNU/Linux, CentOS, ...vérifie si la requête peut être prise en compte
. La requête est transmise à lapplication qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. Elle est ensuite transmise par socket et est prise en compte par un
des _workers_ (= un processus Python) instancié par Gunicorn. Silun de
@ -108,7 +108,7 @@ Les répartiteurs de charges sont super utiles pour donner du mou à linfrast
. Maintenance et application de patches,
. Répartition des utilisateurs connectés,
.
. ...
==== Serveurs dapplication (_Workers_)
@ -167,21 +167,21 @@ ____
Chaque évènement est associé à un niveau; celui-ci indique sa criticité et sa émet un premier tri quant à sa pertinence.
* *DEBUG*: Il sagit des informations qui concernent tout ce qui peut se passer durant lexécution de lapplication. Généralement, ce niveau est désactivé pour une application qui passe en production, sauf sil est nécessaire disoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.
* *INFO*: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement,
* *INFO*: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ...
* *WARN*: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
* *ERROR*: Indique les informations internes - Erreur lors de lappel dune API, erreur interne,
* *FATAL* (ou *EXCEPTION*): généralement suivie dune terminaison du programme - Bind raté dun socket, etc.
* *ERROR*: Indique les informations internes - Erreur lors de lappel dune API, erreur interne, ...
* *FATAL* (ou *EXCEPTION*): ...généralement suivie dune terminaison du programme - Bind raté dun socket, etc.
La configuration des _loggers_ est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore. Il est ainsi possible de définir des formattages, différents gestionnaires (_handlers_) et loggers distincts, en fonction de nos applications.
Sauf que comme nous lavons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux dévènements.
Ilnest donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les `+StreamHandler+`, qui seront pris en charge par gunicorn .
Ilnest donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les `StreamHandler`, qui seront pris en charge par gunicorn .
La configuration que nous allons utiliser est celle-ci :
. *Formattage*: à définir - mais la variante suivante est complète, lisible et pratique:
`+{levelname} {asctime} {module} {process:d} {thread:d} {message}+`
. *Handler*: juste un, qui définit un `+StreamHandler+`
. *Logger*: pour celui-ci, nous avons besoin dun niveau (`+level+`) et de savoir sil faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de `+propagate+` à `+True+`.
`{levelname} {asctime} {module} {process:d} {thread:d} {message}`
. *Handler*: juste un, qui définit un `StreamHandler`
. *Logger*: pour celui-ci, nous avons besoin dun niveau (`level`) et de savoir sil faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de `propagate` à `True`.
Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant :
@ -202,11 +202,11 @@ Des erreurs sur un environnement de production arriveront, tôt ou tard, et sero
Comme nous lavons vu, en fonction de linfrastructure choisie, il existe plusieurs types de journaux:
. Les journaux applicatifs: ie. le flux dévènements généré par votre application Django
. Les journaux du serveur: Nginx, Gunicorn,
. Les journaux du serveur: Nginx, Gunicorn, ...
. Les journaux des autres composants: base de données, service de mise en cache, ...
Une manière de faire consiste à se connecter physiquement ou à distance à la machine pour analyser les logs.
En pratique, cest impossible : entre les répartiteurs de charge, les différents serveurs, , il vous sera physiquement impossible de récupérer une information cohérente.
En pratique, cest impossible : entre les répartiteurs de charge, les différents serveurs, ..., il vous sera physiquement impossible de récupérer une information cohérente.
La solution consiste à agréger vos journaux à un seul et même endroit:
image:infrastructure/mattsegal-logging.png[[align="center"]]
@ -224,13 +224,13 @@ La première étape consiste à agréger ces données dans un dépôt centralis
La collecte des données doit récupérer des données des couches métiers, applicatives et denvironnement.
Ces données couvrent les évènements, les journaux et les métriques - indépendamment de leur source - le pourcentage dutilisation du processeur, la mémoire utilisée, les
disques systèmes, lutilisation du réseau,
disques systèmes, lutilisation du réseau, ...
. *Métier*: Le nombre de ventes réalisées, le nombre de nouveaux utilisateurs, les résultats de tests A/B,
. *Application*: Le délai de réalisation dune transaction, le temps de réponse par utilisateur,
. *Infrastructure*: Le trafic du serveur Web, le taux doccupation du CPU,
. *Côté client*: Les erreurs applicatives, les transactions côté utilisateur,
. *Pipeline de déploiement*: Statuts des builds, temps de mise à disposition dune fonctionnalité, fréquence des déploiements, statuts des environnements,
. *Métier*: Le nombre de ventes réalisées, le nombre de nouveaux utilisateurs, les résultats de tests A/B, ...
. *Application*: Le délai de réalisation dune transaction, le temps de réponse par utilisateur, ...
. *Infrastructure*: Le trafic du serveur Web, le taux doccupation du CPU, ...
. *Côté client*: Les erreurs applicatives, les transactions côté utilisateur, ...
. *Pipeline de déploiement*: Statuts des builds, temps de mise à disposition dune fonctionnalité, fréquence des déploiements, statuts des environnements, ...
Bien utilisés, ces outils permettent de prévenir des incidents de manière empirique.

View File

@ -0,0 +1 @@
== Administration auto-générée

View File

@ -208,7 +208,7 @@ class Bidule(models.Model):
raise ValidationError('Title does not start with T')
----
Il nest plus nécessaire de définir dattribut `validators=[]`, puisque Django va appliquer un peu dintrospection pour récupérer toutes les méthodes qui commencent par `clean_` et pour les faire correspondre au nom du champ à valider (ici, `title`).
Il nest plus nécessaire de définir dattribut `validators=[...]`, puisque Django va appliquer un peu dintrospection pour récupérer toutes les méthodes qui commencent par `clean_` et pour les faire correspondre au nom du champ à valider (ici, `title`).
==== Clean
@ -282,7 +282,7 @@ Nous obtenons ainsi une structure représentée par un dictionnaire, qui décrit
=== Dépendance avec le modèle
Un *form* peut hériter dune autre classe Django.
Pour cela, il suffit de fixer lattribut `model` au niveau de la `class Meta` dans la définition.
Pour cela, il suffit de fixer lattribut `model` au niveau de la `class Meta` dans la définition.
[source,python]
----
@ -298,7 +298,7 @@ Pour cela, il suffit de fixer lattribut `model` au niveau de la `class Meta`
----
Notre form dépendra automatiquement des champs déjà déclarés dans la classe `Wishlist`.
Cela suit le principe de DRY <dont repeat yourself  et évite quune modification ne pourrisse le code: en testant les deux champs présent dans lattribut fields, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base.
Cela suit le principe de DRY <dont repeat yourself et évite quune modification ne pourrisse le code: en testant les deux champs présent dans lattribut fields, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base.
=== Rendu et affichage
@ -323,7 +323,7 @@ Comme on la vu à linstant, les forms, en Django, cest le bien.
Cela permet de valider des données reçues en entrée et dafficher (très) facilement des formulaires à compléter par lutilisateur.
Par contre, cest lourd.
Dès quon souhaite peaufiner un peu laffichage, contrôler parfaitement ce que lutilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près,
Dès quon souhaite peaufiner un peu laffichage, contrôler parfaitement ce que lutilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, ...
Tout ça demande énormément de temps. Et cest là quintervient http://django-crispy-forms.readthedocs.io/en/latest/[Django-Crispy-Forms].
Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le *layout* et la présentation.

View File

@ -69,7 +69,7 @@ La définition d'un ordre par défaut au niveau du modèle ne signifie pas qu
Il est donc nécessaire de tester ce type de comportement _dans son ensemble_, dans la mesure où un `.first()` dans votre code (l'équivalent du `TOP 1` en SQL) pourrait être modifié simplement par cette petite information.
====
TIP: Pour sélectionner un objet au pif: `return Category.objects.order_by("?").first()`
TIP: Pour sélectionner un objet au pif: `return Category.objects.order_by("?").first()`
=== Contraintes

View File

@ -198,7 +198,7 @@ class Migration(migrations.Migration):
]
----
* La migration supprime lancienne clé étrangère
* La migration supprime lancienne clé étrangère ...
* ... et ajoute une nouvelle table, permettant de lier nos catégories à nos
livres.
@ -301,7 +301,7 @@ TODO intégrer ici un point sur les updates db - voir designing data-intensive a
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code, et doivent faire partie du
dépôt central de sources. Le développeur soccupe de créer les migrations en fonction des actions à entreprendre; ces migrations
peuvent être retravaillées, _squashées_, et feront partie intégrante du processus de mise à jour de lapplication.
peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de lapplication.
A noter que les migrations nappliqueront de modifications que si le schéma est impacté. Ajouter une propriété `related_name` sur une
ForeignKey nengendrera aucune nouvelle action de migration, puisque ce type daction ne sapplique que sur lORM, et pas directement sur la
@ -417,7 +417,7 @@ class Migration(migrations.Migration):
]
----
La commande `python manage.py squashmigrations library 0002 0003` appliquera une fusion entre les migrations numérotées `0002` et `0003`:
La commande `python manage.py squashmigrations library 0002 0003` appliquera une fusion entre les migrations numérotées `0002` et `0003`:
[source,shell]
----
@ -491,4 +491,4 @@ En résumé:
`__init__.py`)
. Soit on réinitialise proprement les migrations avec un `fake-initial`
(sous réserve que toutes les personnes qui utilisent déjà le projet sy
conformentCe qui nest pas gagné.
conforment...Ce qui nest pas gagné.

View File

@ -631,7 +631,7 @@ Cela nous dit plus ou moins de quoi il s'agit (_C'est un livre, et il porte l'id
==== Nomenclature des relations
epuis le code, à partir de linstance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance.
*A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
*A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter lattribut `related_name` afin de nommer la relation inverse.
@ -740,7 +740,7 @@ Nous pensons ici à un UUID ou à un slug , qui permettrait davoir une sorte
==== Héritage classique
Lhéritage classique est généralement déconseillé, car il peut introduire très rapidement un problème de performances: en reprenant lexemple introduit avec lhéritage par classe abstraite, et en omettant lattribut `abstract = True`, on se retrouvera en fait avec quatre tables SQL:
Lhéritage classique est généralement déconseillé, car il peut introduire très rapidement un problème de performances: en reprenant lexemple introduit avec lhéritage par classe abstraite, et en omettant lattribut `abstract = True`, on se retrouvera en fait avec quatre tables SQL:
* Une table `AbstractModel`, qui reprend les deux champs `created_at` et
`updated_at`

View File

@ -103,8 +103,8 @@ souhaits dont le nom contient (avec une casse insensible) la chaîne
Pour un OR, on a deux options :
. Soit passer par deux querysets, typiuqment `queryset1  queryset2`
. Soit passer par des `Q objects`, que lon trouve dans le namespace
. Soit passer par deux querysets, typiuqment `queryset1 queryset2`
. Soit passer par des `Q objects`, que lon trouve dans le namespace
`django.db.models`.
[source,python]

View File

@ -189,14 +189,14 @@ Le fonctionnement de la méthode `.prefetch_related()` est identique à la méth
[WARNING]
====
Il faut faire attention au prefetch related, qui fonctionne grâce à une grosse requête dans laquelle nous trouvons un `IN (...)`.
Il faut faire attention au prefetch related, qui fonctionne grâce à une grosse requête dans laquelle nous trouvons un `IN (...)`.
Dans cette optique,
1. Django récupère tous les objets demandés,
2. Pour ensuite prendre toutes les clés primaires,
3. Pour finalement faire une deuxième requête et récupérer les relations externes.
Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données), cette seconde requête pourrait ne pas fonctionner et vous obtiendrez une exception de type `django.db.utils.OperationalError: too many SQL variables`, qu'il conviendra de gérer correctement.
Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données), cette seconde requête pourrait ne pas fonctionner et vous obtiendrez une exception de type `django.db.utils.OperationalError: too many SQL variables`, qu'il conviendra de gérer correctement.
Nous pourrions penser quutiliser un itérateur permettrait de combiner les deux, mais ce nest pas le cas.
Comme lindique la documentation:

View File

@ -14,15 +14,9 @@ Si vous manquez didées ou si vous ne savez pas par où commencer :
* https://code.visualstudio.com/[Visual Studio Code] ou https://vscodium.com/[VSCodium], avec le greffon
https://marketplace.visualstudio.com/items?itemName=ms-python.python[Python]
* https://www.jetbrains.com/pycharm/[PyCharm]
* https://www.vim.org/[Vim] avec les greffons
https://github.com/davidhalter/jedi-vim[Jedi-Vim] et
https://github.com/preservim/nerdtree[nerdtree].
* https://www.vim.org/[Vim] avec les greffons https://github.com/davidhalter/jedi-vim[Jedi-Vim] et https://github.com/preservim/nerdtree[nerdtree].
Si vous hésitez, et même si Codium nest pas le plus léger (la faute à
https://www.electronjs.org/[Electron]...), il fera correctement son
travail (à savoir : faciliter le vôtre), en intégrant suffisament de
fonctionnalités qui gâteront les papilles émoustillées du développeur
impatient.
Si vous hésitez, et même si Codium nest pas le plus léger (la faute à https://www.electronjs.org/[Electron]...), il fera correctement son travail (à savoir : faciliter le vôtre), en intégrant suffisament de fonctionnalités qui gâteront les papilles émoustillées du développeur impatient.
image::environment/codium.png[align="center"]
@ -149,15 +143,15 @@ normes PCI parce les données des titulaires de cartes ne sont pas isolées corr
Il suffit alors de:
. Sauver le travail en cours (`git add . && git commit -m [WIP]`)
. Revenir sur la branche principale (`git checkout main`)
. Créer un "hotfix" (`git checkout -b hotfix/pci-compliance`)
. Sauver le travail en cours (`git add . && git commit -m [WIP]`)
. Revenir sur la branche principale (`git checkout main`)
. Créer un "hotfix" (`git checkout -b hotfix/pci-compliance`)
. Solutionner le problème (sans doute un `;` en trop ?)
. Sauver le correctif sur cette branche
(`git add . && git commit -m "Did it!"`)
(`git add . && git commit -m "Did it!"`)
. Récupérer ce correctif sur la branche principal
(`git checkout main && git merge hotfix/pci-compliance`)
. Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que lunivers vous réclame à cor et à a cri (`git checkout features/dinolol`)
(`git checkout main && git merge hotfix/pci-compliance`)
. Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que lunivers vous réclame à cor et à a cri (`git checkout features/dinolol`)
Il existe plusieurs méthodes pour gérer ces flux dinformations.
Les plus connues sont https://www.gitflow.com/[Gitflow] et
@ -183,7 +177,7 @@ articles pour débuter avec Git:
==== Décrire ses changements
La description dun changement se fait _via_ la commande `git commit`.
La description dun changement se fait _via_ la commande `git commit`.
Il est possible de lui passer directement le message associé à ce
changement grâce à lattribut `-m`, mais cest une pratique relativement
déconseillée: un _commit_ ne doit effectivement pas obligatoirement être
@ -322,29 +316,63 @@ image::apps/bruno.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:
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
#
```
# 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
# 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
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
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."
....
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
```
Pour conclure, `make` peu sembler un peu désuet, mais reste extrêmement efficace.
=== Licence
[IMPORTANT]
====
Choisissez une licence.
====
Si votre projet n'a pas de licence qui lui est associée, 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.
Une https://github.com/facebook/react/issues/7293[_issue_] a par exemple été adressée en 2017 sur la librairie _React_, et qui aurait pu créer un conflit de revendication de brevet.
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.
Parmi toutes les licences existant, trois licences d'entre elles 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 https://sidekiq.org/[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._
____
Pour la petite histoire, `+make+` peu sembler un peu désuet, mais reste extrêmement efficace.
=== Conclusions
@ -355,11 +383,12 @@ Pour résumer :
. Prenez un gestionnaire de mot de passe (vous en aurez besoin par la suite pour gérer vos clés et mots de passe),
. Appréhendez Git - c'est le standard actuel dans l'industrie,
. Choisissez un moteur de base de données (et si vous ne savez pas lequel, contentez-vous d'SQLite dans un premier temps),
. Compétez votre ceinture à outils avec tous ceux que vous jugerez bons de retenir.
. Compétez votre ceinture à outils avec tous ceux que vous jugerez bons de retenir,
. Choisissez une licence d'utilisation.
Pour récapituler les différents outils que nous proposons :
* IDE : VSCode,
* IDE : VSCode ou PyCharm,
* Langage de programmation : Python,
* Suivi des versions : Git,
* Automatisation des tâches : Make.

View File

@ -298,7 +298,7 @@ parente. Mathématiquement, ce principe peut être défini de la manière
suivante:
____
[ if S is a subtype of T, then objects of type T in a computer program
[... if S is a subtype of T, then objects of type T in a computer program
may be replaced with objects of type S (i.e., objects of type S may be
substituted for objects of type T), without altering any of the
desirable properties of that program (correctness, task performed,
@ -392,7 +392,7 @@ class MarkdownRenderer(XmlRenderer):
return markdown.markdown(document.content)
----
Le code ci-dessus ne porte pas immédiatement à conséquence, mais dès que nous invoquerons la méthode `header()` sur une instance de type `MarkdownRenderer`, nous obtiendrons un bloc de déclaration XML (`<?xml version = "1.0"?>`) pour un fichier Markdown, ce qui naura aucun sens.
Le code ci-dessus ne porte pas immédiatement à conséquence, mais dès que nous invoquerons la méthode `header()` sur une instance de type `MarkdownRenderer`, nous obtiendrons un bloc de déclaration XML (`<?xml version = "1.0"?>`) pour un fichier Markdown, ce qui naura aucun sens.
En revenant à notre proposition dimplémentation, suite au respect dOpen-Closed, une solution serait de nimplémenter la méthode `header()` quau niveau de la classe `XmlRenderer` :
@ -898,7 +898,7 @@ Avec cette approche, les composants seront déjà découplés au mieux.
Les composants peuvent être découpés au niveau:
* *Du code source*, via des modules, paquets, dépendances,
* *Du code source*, via des modules, paquets, dépendances, ...
* *Du déploiement ou de lexécution*, au travers de dll, jar, linked libraries, ..., voire au travers de threads ou de processus locaux.
* *Via la mise à disposition de nouveaux services*, lorsquil est plus intuitif de contacter un nouveau point de terminaison que dintégrer de force de nouveaux concepts dans une base de code existante.

View File

@ -59,7 +59,7 @@ En plus de cela, chaque développeur a une copie de lapplication qui fonction
====
Comme lexplique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure: "_Un des avantages dutiliser un dépôt unique de sources, est quil permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination_ cite:[devops_handbook(288-298)].
Ce dépôt nest pas uniquement destiné à hébergé le code source, mais également à dautres artefacts et autres formes de connaissance, comme les standards de configuration (Chef recipes, Puppet manifests, ), outils de déploiement, standards de tests (y compris ce qui touche à la sécurité), outils danalyse et de monitoring ou tutoriaux.
Ce dépôt nest pas uniquement destiné à hébergé le code source, mais également à dautres artefacts et autres formes de connaissance, comme les standards de configuration (Chef recipes, Puppet manifests, ...), outils de déploiement, standards de tests (y compris ce qui touche à la sécurité), outils danalyse et de monitoring ou tutoriaux.
[[dependencies]]
=== Déclaration explicite et isolation des dépendances
@ -74,7 +74,7 @@ Lors de la création dun nouvel environnement vierge, il suffira dutiliser
====
Il est important de bien "épingler" la version liée à chacune des dépendances de lapplication.
Ceci peut éviter des effets de bord comme une nouvelle version dune librairie dans laquelle un bug aurait pu avoir été introduit.footnote:[Le paquet PyLint dépend par exemple dAstroid; https://github.com/PyCQA/pylint-django/issues/343[en janvier 2022], ce dernier a été mis à jour sans respecter le principe de versions sémantiques et introduisant une régression. PyLint spécifiait que sa dépendance avec Astroid devait respecter une version  2.9.
Ceci peut éviter des effets de bord comme une nouvelle version dune librairie dans laquelle un bug aurait pu avoir été introduit.footnote:[Le paquet PyLint dépend par exemple dAstroid; https://github.com/PyCQA/pylint-django/issues/343[en janvier 2022], ce dernier a été mis à jour sans respecter le principe de versions sémantiques et introduisant une régression. PyLint spécifiait que sa dépendance avec Astroid devait respecter une version 2.9.
Lors de sa mise à jour en 2.9.1, Astroid a introduit un changement majeur, qui faisait planter Pylint.
Lépinglage explicite aurait pu éviter ceci.].
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.
@ -88,7 +88,7 @@ La majorité des langages modernes proposent des mécanismes similaires :
* https://rubygems.org/[Gem] pour Ruby,
* https://www.npmjs.com/[NPM] pour NodeJS,
* link:Maven[https://maven.apache.org/] pour Java,
*
* ...
=== Configuration applicative
@ -148,7 +148,7 @@ Le risque de se retrouver avec une liste colossale de paramètres nest cepend
=== Ressources externes
Nous parlons de bases de données, de services de mise en cache, dAPI externes,
Nous parlons de bases de données, de services de mise en cache, dAPI externes, ...
Lapplication doit être capable deffectuer des changements au niveau de ces ressources sans que son code ne soit modifié.
Nous parlons alors de *ressources attachées*, dont la présence est nécessaire au bon fonctionnement de lapplication, mais pour lesquelles le *type* nest pas obligatoirement défini.
@ -172,7 +172,7 @@ Nous serons ainsi ravis de pouvoir simplement modifier la chaîne de connexion `
image::12-factors/separate-run-steps.png[align="center"]
Parmi les solutions possibles, nous pourrions nous baser sur les _releases_ de Gitea, sur un serveur dartefacts (https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano]), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, ).
Parmi les solutions possibles, nous pourrions nous baser sur les _releases_ de Gitea, sur un serveur dartefacts (https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano]), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, ...).
Dans le cas de Python, la phase de construction (_Build_) correspondra plutôt à une phase d_empaquettage_ (_packaging_).
Une fois préparée, la librairie ou lapplication pourra être publiée sur pypi.org ou un dépôt géré en interne.

View File

@ -31,7 +31,8 @@ Les sauts de versions sont bien documentés, et ne nécessitent que quelques heu
. Créez un nouveau répertoire de travail (`mkdir generic`)
. Déplacez l'invite de commande dans ce nouveau répertoire (`cd generic`)
. Initialiser un nouvel espace pour Poetry (`poetry init --no-interaction`)
. Ajoutez-y Django comme dépendance (`poetry add django`).
. Ajoutez-y Django comme dépendance (`poetry add django`),
. Démarrez votre serveur de développement (`poetry run python manage.py runserver`).
```bash
@ -41,13 +42,16 @@ poetry run django-admin startproject gwift .
poetry run python manage.py runserver
```
Ici, la commande `+pip install django+` récupère la *dernière version connue disponible dans les dépôts https://pypi.org/*.
[IMPORTANT]
====
La commande `poetry add django` récupère la *dernière version connue disponible dans les dépôts https://pypi.org/*.
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 (cf. <<dependencies>>).
====
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+`.
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 plutôt la commande `manage.py`, qui constitue un *wrapper* autour de `django-admin`.
Pour démarrer notre projet, nous lançons `+django-admin startproject gwift+` au travers de Poetry :
Pour démarrer notre projet, nous lançons `poetry run django-admin startproject gwift` au travers de Poetry :
```bash
$ poetry run django-admin startproject gwift . <1>
@ -63,11 +67,11 @@ $ tree .
.
├── db.sqlite3
├── gwift
   ├── __init__.py
   ├── asgi.py
   ├── settings.py
   ├── urls.py
   └── wsgi.py
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
├── manage.py
├── poetry.lock
└── pyproject.toml
@ -78,17 +82,15 @@ Le but est de faire en sorte que toutes les opérations de maintenance, déploie
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
* `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],
* `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
* `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.
[NOTE]
====
@ -97,24 +99,24 @@ Il est ainsi conseillé de créer un fichier `requirements.txt` qui pointera ver
Par soucis de propreté, placez ces différents fichiers dans un dossier `requirements`, que vous placerez à la racine de votre projet.
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>+`.
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>`.
A noter également qu'une bonne pratique consiste à placer un fichier `+requirements.txt+` à la racine du projet, et dans lequel nous
retrouverons le contenu `+-r requirements/production.txt+`, notamment pour Heroku.
A noter également qu'une bonne pratique consiste à placer un fichier `requirements.txt` à la racine du projet, et dans lequel nous
retrouverons le contenu `-r requirements/production.txt`, notamment pour Heroku.
====
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.
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.
=== Django
Comme nous lavons vu ci-dessus, `+django-admin+` permet de créer un nouveau projet.
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.
* *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 :
Pour `gwift`, nous aurons :
.Projet Django vs Applications
image::django/django-project-vs-apps-gwift.png[align="center"]
@ -125,30 +127,19 @@ image::django/django-project-vs-apps-gwift.png[align="center"]
. 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.
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:
Pour `khana`, nous pourrions avoir quelque chose comme ceci :
.Django Project vs Applications
image::django/django-project-vs-apps-khana.png[align="center"]
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], ...)
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.
Le projet soccupe donc principalement dappliquer une couche de glue entre différentes applications, là où l'application est sensée composer une entité autonome et réutilisable.
[NOTE]
====
@ -160,17 +151,17 @@ Une autre bonne pratique consiste à aussi *limiter à cinq* le nombre de modèl
Tant que ce seuil ne sera pas atteint, ne vous occupez pas de ce principe.
====
==== manage.py
=== Commandes de gestion
Le fichier `+manage.py+` que vous trouvez à la racine de votre projet est un *wrapper* sur les commandes `+django-admin+`.
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.
* `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+`.
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,
@ -180,11 +171,11 @@ Vous remarquerez que ces commandes sont groupées selon différentes catégories
* *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+`.
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 `+poetry run python manage.py runserver+`, nous verrons la sortie console suivante :
Si nous démarrons la commande `poetry run python manage.py runserver`, nous verrons la sortie console suivante :
```bash
$ poetry run python manage.py runserver
@ -204,13 +195,13 @@ image::django/manage-runserver.png[align="center"]
[NOTE]
====
Si vous avez suivi les étapes jusquici, vous avez également dû voir un message type `You have 18 unapplied migration(s).`
Si vous avez suivi les étapes jusquici, vous avez également dû voir un message type `You have 18 unapplied migration(s).`
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 `poetry run python manage.py startapp <label>`.
Maintenant que nous avons a vu à quoi servait `manage.py`, nous pouvons créer notre nouvelle application grâce à la commande `poetry run python 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.
@ -224,147 +215,126 @@ $ poetry run python manage.py startapp wish gwift/wish <2>
<2> Nous exécutons ensuite le _scaffolding_ de création de cette application, dans le répertoire pré-cité. Cette deuxième étape est également discutée en annexe, dans la section <<Différentes structures de projets>>.
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.
Résultat ? Django nous a créé un répertoire `+wish+`, dans lequel nous
trouvons les fichiers et dossiers suivants:
La structure de vos répertoires devient celle-ci :
* `+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, …]
```bash
% tree .
.
├── db.sqlite3
├── gwift
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ ├── wish <1>
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ └── wsgi.py
├── manage.py
├── poetry.lock
└── pyproject.toml
```
<1> C'est dans ce répertoire que nous retrouvons les fichiers composant notre application.
=== 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,
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).
Django (et dautres frameworks) résolvent ce problème en se basant ouvertement sur le principe de `Dont repeat yourself` (((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.
Le chemin parcouru par une requête peut être repris schématiquement comme si *un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application*.
Nous prendrons 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::django/django-how-it-works.png[align="center"]
*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+`.
*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`.
Ce fichier reprendra
[source,Python]
----
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
```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),
]
----
urlpatterns = [
path('admin/', admin.site.urls), <1>
path("wishes/<int:wish_id>", wish_details), <2>
]
```
<1> Cette partie-ci est auto-générée, et sera vue dans le chapitre <<Administration>>
<2> Nous insérons ici notre nouvelle URL (((URL)))
*Etape 1* - Si ce nest pas le cas, lapplication nira pas plus loin et
retournera une erreur à lutilisateur.
*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+`.
*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 correspondant au deuxième paramètre que nous trouvons dans l'appel à la fonction `path()`, c'est-à-dire ici la fonction `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+`
[NOTE]
====
Ce deuxième paramètre 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_.
====
Le module `+gwift.views+` qui se trouve dans le fichier
`+gwift/views.py+` peut ressembler à ceci:
* Nous importons la fonction `wish_details` du module `gwift.views`,
* Champomy et cotillons ! Nous avons une correspondance avec `wishes/details/91827`.
[source,Python]
----
# gwift/views.py
Le module `gwift.views` qui se trouve dans le fichier `gwift/views.py` peut ressembler à ceci:
[...]
```python
# gwift/views.py
from datetime import datetime
[...]
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
)
----
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:
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é.
. 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 -->
<!-- templates/wish_details.html -->
<!DOCTYPE html>
<html>
<head>
@ -378,9 +348,7 @@ propre à notre application.
</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:
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]
----
@ -398,365 +366,7 @@ la requête initiale:
----
.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
....
# 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
....
==== 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._
____
image::django/django-first-template.png[images/django/django-first-template]
=== Conclusions
@ -764,4 +374,4 @@ 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,
dajouter un mécanisme de mise en cache, des logiciels de suivi, ...

View File

@ -217,7 +217,7 @@ sortira très bien et sera beaucoup plus efficace que nimporte quelle
chaîne de caractères que vous pourriez indiquer et qui sera fausse dans
six mois,
* **Inutile de décrire quelque chose qui est évident** : documenter la
méthode `+get_age()+` dune personne naura pas beaucoup dintérêt,
méthode `get_age()` dune personne naura pas beaucoup dintérêt,
* Sil est nécessaire de décrire un comportement au sein-même dune
fonction, avec un commentaire _inline_, cest que ce comportement
pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être
@ -230,7 +230,7 @@ Il existe plusieurs types de balisages reconnus :
. RestructuredText
. Numpy
. Google Style (parfois connue sous lintitulé `+Napoleon+`)
. Google Style (parfois connue sous lintitulé `Napoleon`)
... mais tout système de balisage peut être reconnu, sous réseve de
respecter la structure de la https://peps.python.org/pep-0257/[PEP257], qui donne des recommandations haut-niveau concernant la structure des docstrings : ce quelles doivent contenir et comment lexpliciter, sans imposer quelle que mise en forme de contenu que ce soit.
@ -261,13 +261,13 @@ la manière dont limplémentation est réalisée). (_Extended summary_)
* La ou les valeurs de retour, accompagnées de leur type et dune
description (_Returns_)
* Les valeurs générées (_yields_) dans le cas de générateurs (_Yields_)
* Un objet attendu par la méthode `+send()+` (toujours dans le cas de
* Un objet attendu par la méthode `send()` (toujours dans le cas de
générateurs) (_Receives_)
* Dautres paramètres, notamment dans le cas des paramètres `+*args+` et
`+**kwargs+`. (_Other parameters_)
* Dautres paramètres, notamment dans le cas des paramètres `*args` et
`**kwargs`. (_Other parameters_)
* Les exceptions levées (_Raises_)
* Les avertissements destinés à lutilisateur (_Warnings_)
* Une section "Pour continuer" (_See also_)
* Une section "Pour continuer..." (_See also_)
* Des notes complémentaires (_Notes_)
* Des références (_References_)
* Des exemples (_Examples_)
@ -497,7 +497,7 @@ test.py:18:1: W391 blank line at end of file
Nous trouvons des erreurs:
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre despace après un opérateur, une ligne vide à la fin du fichier, Ces _erreurs_ nen sont pas vraiment, elles indiquent juste de
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre despace après un opérateur, une ligne vide à la fin du fichier, ... Ces _erreurs_ nen sont pas vraiment, elles indiquent juste de
potentiels problèmes de communication si le code devait être lu ou compris par une autre personne.
* de *définition*: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug
potentiel. Ne pas en tenir compte nuira sans doute à la santé de votre code (et risque de vous réveiller à cinq heures du mat, quand votre application se prendra méchamment les pieds dans le tapis).
@ -552,7 +552,7 @@ mises en forme, que lon a vues ci-dessus)
que le traitement na pas pu aboutir.
Pylint propose également une option particulièrement efficace, qui prend
le paramètre `+errors-only+`, et qui naffiche que les occurrences
le paramètre `errors-only`, et qui naffiche que les occurrences
appartenant à la catégorie *E*.
Connaissant ceci, il est extrêmement pratique dintégrer cette option au niveau des processus dintégration continue, puisque la présence dune erreur détectée par Pylint implique généralement un correctif à appliquer.
@ -563,7 +563,7 @@ peut être:
. *Une ligne de code*
. *Un bloc de code* : une fonction, une méthode, une classe, un module,
...
. *Un projet entier*
Lui, cest le petit chouchou à la mode.
@ -612,9 +612,9 @@ content instead.
____
Traduit rapidement à partir de la langue de Batman :
"`+En utilisant Black, vous cédez le contrôle sur le formatage de votre code. En retour, Black vous fera gagner un max de temps, diminuera votre charge mentale et fera revenir lêtre aimé+`".
"`En utilisant Black, vous cédez le contrôle sur le formatage de votre code. En retour, Black vous fera gagner un max de temps, diminuera votre charge mentale et fera revenir lêtre aimé`".
Mais la partie réellement intéressante concerne le fait que
"`+Tout code qui sera passé par Black aura la même forme, indépendamment du project sur lequel vous serez en train de travailler. Létape de formatage deviendra transparente, et vous pourrez vous concentrer sur le contenu+`".
"`Tout code qui sera passé par Black aura la même forme, indépendamment du project sur lequel vous serez en train de travailler. Létape de formatage deviendra transparente, et vous pourrez vous concentrer sur le contenu`".
=== Typage statique
@ -651,7 +651,7 @@ a
Malgré que nos annotations déclarent une liste dentiers, rien ne nous empêche de lui envoyer une liste de caractères, sans que cela ne lui pose de problèmes. La signature de notre fonction nest donc pas cohérente avec son comportement.
Est-ce que Mypy va râler ? *Oui, aussi*. Non seulement nous retournons la valeur `+None+` si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous lappelons avec une liste de caractères, alors que nous nous attendons à une liste dentiers :
Est-ce que Mypy va râler ? *Oui, aussi*. Non seulement nous retournons la valeur `None` si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous lappelons avec une liste de caractères, alors que nous nous attendons à une liste dentiers :
....
>>> mypy mypy-test.py
@ -664,8 +664,8 @@ Found 4 errors in 1 file (checked 1 source file)
Pour corriger ceci, nous devons :
. Importer le type `+Optional+` et lutiliser en sortie de notre
fonction `+first_int_elem+`
. Importer le type `Optional` et lutiliser en sortie de notre
fonction `first_int_elem`
. Eviter de lui donner de mauvais paramètres ;-)²
Mypy demande une rigueur supplémentaire, qui peut s'avérer extrêmement utile sur des grosses bases de code ou avec de larges équipes, mais qui nécessite également un gros travail d'intégration, qui pourrait potentiellement ralentir le travail.
@ -848,4 +848,4 @@ facteurs → Construction du fichier setup.cfg
django_coverage_plugin
....
Mypy + black + pylint + flake8 + pyflakes +
Mypy + black + pylint + flake8 + pyflakes + ...

View File

@ -51,7 +51,7 @@ fonction, et ne doit pas obligatoirement faire partie dune classe héritant d
* 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
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]
@ -108,7 +108,7 @@ Il ne sagit pas de vérifier que le code est bien testé, mais de vérifier q
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/+`.
`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
@ -144,11 +144,11 @@ $ coverage report
$ coverage html
....
Avec `+pytest+`, il convient dutiliser le paquet https://pypi.org/project/pytest-cov/[`+pytest-cov+`], suivi de la commande `+pytest --cov=gwift tests/+`.
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.
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.
=== Matrice de compatibilité
@ -173,7 +173,7 @@ La documentation de Poetry spécifie https://python-poetry.org/docs/faq/#is-tox-
A noter que pour que les commandes ci-dessus fonctionnent correctement, il sera nécessaire que vous ayez les différentes versions dinterpréteurs installées ou accessibles.
Une solution consiste à utiliser `pyenv`, qui s'occupera d'installer les versions devant l'être, afin d'éviter les erreurs de type `ERROR:   pyXX: InterpreterNotFound: pythonX.X` :
Une solution consiste à utiliser `pyenv`, qui s'occupera d'installer les versions devant l'être, afin d'éviter les erreurs de type `ERROR: pyXX: InterpreterNotFound: pythonX.X` :
....
pyenv install 3.10
@ -196,3 +196,244 @@ Pour atteindre cet objectif, il est nécessaire :
. De disposer de tests automatisés,
. De scénarii de déploiement,
. D'une télémétrie omniprésente.
=== Tests
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
Pour valider la couverture de code, nous aurons besoin de `coverage` et de `django_coverage_plugin`.
Ajoutez les au groupe de dépendances en test uniquement, puis ajoutez un fichier de configuration, qui sera valable quel que soit l'environnement de développement qui sera concerné :
....
# .coveragerc
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
....
==== 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.

View File

@ -28,9 +28,9 @@ Because value is created only when our services are running into production, we
— DevOps Handbook Introduction
____
Le serveur que Django met à notre disposition _via_ la commande `+runserver+` est extrêmement pratique, mais il est uniquement prévu pour la phase développement. En production :
Le serveur que Django met à notre disposition _via_ la commande `runserver` est extrêmement pratique, mais il est uniquement prévu pour la phase développement. En production :
* Il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que lon connait ses limites et que lon se limite à un utilisateur à la fois.
* Il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ... De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que lon connait ses limites et que lon se limite à un utilisateur à la fois.
* Il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés. En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce quun autre processus a déjà pris la main pour écrire ses données. En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou
ou simplement parce que le serveur répondra trop lentement.
@ -79,7 +79,7 @@ Vous noterez que lensemble des composants utilisés sont _Open source_ :
____
When I built my first company starting in 1999, it cost $2.5 million in
infrastructure just to get started and another $2.5 million in team
costs to code, launch, manage, market and sell our software [].
costs to code, launch, manage, market and sell our software [...].
The first major change in our industry was imperceptible to us as an
industry. It was driven by the introduction of open-source software,

View File

@ -37,3 +37,5 @@ include::book/django-principles/migrations.adoc[]
include::book/django-principles/querysets+managers.adoc[]
include::book/django-principles/forms.adoc[]
include::book/django-principles/admin.adoc[]

View File

@ -55,8 +55,8 @@ 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[]
include::book/environment/tests.adoc[]