Move several files to make 'django' section more consistent

* Delete venvs.adoc and merge it with 'working in isolation'
* Delete tools.adoc and merge it with 'the python language'
* Move migrations to django concepts
* Move unit_tests to django concepts
This commit is contained in:
Fred 2020-12-16 21:33:45 +01:00
parent 40c01c9a87
commit 86bd6fbfda
9 changed files with 402 additions and 426 deletions

View File

@ -64,6 +64,8 @@ A présent que l'environnement est activé, tous les binaires de cet environneme
De la même manière, une variable `PATH` propre est définie et utilisée, afin que les librairies Python y soient stockées.
C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les aurons installées.
NOTE: Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire dans lequel se trouve une installation fraîche de l'interpréteur, vers laquelle pointe les liens symboliques des binaires. Si vous recherchez l'emplacement de l'interpréteur avec la commande `which python`, vous recevrez comme réponse `/home/fred/.venvs/gwift-env/bin/python`.
Pour sortir de l'environnement virtuel, exécutez la commande `deactivate`.
Si vous pensez ne plus en avoir besoin, supprimer le dossier.
Si nécessaire, il suffira d'en créer un nouveau.
@ -212,7 +214,7 @@ Nous pouvons clairement visualiser le principe de **contexte** pour une applicat
C'est 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], ...).
=== manage.py
==== 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 n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet:
@ -260,48 +262,47 @@ Si vous avez suivi les étapes jusqu'ici, vous avez également dû voir un messa
Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
==== Création d'une nouvelle application
TODO: JE ME SUIS ARRETE ICI <----
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 les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application.
Pour nous, ce sera donc `wish`.
=== Structure d'une application
Maintenant que l'on a vu à quoi servait `manage.py`, on peut créer notre nouvelle application grâce à la commande `manage.py startapp <label>`.
Cette application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir. Essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc `wish`. C'est parti pour `manage.py startapp wish`!
C'est parti pour `manage.py startapp wish`!
[source,bash]
----
$ python manage.py startapp wish
----
Résultat? Django nous a créé un répertoire `wish`, dans lequel on trouve les fichiers et dossiers suivants:
Résultat? Django nous a créé un répertoire `wish`, dans lequel nous trouvons les fichiers et dossiers suivants:
* `wish/admin.py` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* `wish/__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `wish/migrations/`, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* `wish/models.py` pour représenter et structurer nos données.
* `wish/admin.py` servira à structurer l'administration de notre application. Chaque information peut être administrée facilement au travers d'une interface générée à la volée par le framework. Nous y reviendrons par la suite.
* `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, et est intimement lié aux migrations.
* `wish/tests.py` pour les tests unitaires.
Par soucis de clarté, déplacez ce nouveau répertoire `wish` dans votre répertoire `gwift` existant. C'est une forme de convention. La structure de vos répertoires devient celle-ci:
NOTE: Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire `wish` dans votre répertoire `gwift` existant.
C'est une forme de convention.
La structure de vos répertoires devient celle-ci:
[source,bash]
----
(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── docs
│   └── README.md
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── wish <1>
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── __init__.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
@ -310,6 +311,7 @@ Par soucis de clarté, déplacez ce nouveau répertoire `wish` dans votre réper
│   └── wsgi.py
├── Makefile
├── manage.py
├── README.md
├── requirements
│   ├── base.txt
│   ├── dev.txt
@ -317,57 +319,63 @@ Par soucis de clarté, déplacez ce nouveau répertoire `wish` dans votre réper
├── setup.cfg
└── tox.ini
6 directories, 22 files
5 directories, 22 files
----
<1> Notre application a bien été créée, et on l'a déplacée dans le répertoire `gwift` !
* `admin.py` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* `__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `migrations/`, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* `models.py` pour représenter et structurer nos données.
* `tests.py` pour les tests unitaires.
<1> Notre application a bien été créée, et nous l'avons déplacée dans le répertoire `gwift` !
=== Migrations et schéma de bases de données
==== Fonctionement général
https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html[reset migrations].
-> diagramme django
En gros, soit on supprime toutes les migrations (en conservant le fichier __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 s'y conforment... Ce qui n'est pas gagné.
=== Tests unitaires
==== 12 facteurs et configuration globale
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* `views.py` pour définir ce que nous pouvons faire avec nos données.
-> Faire le lien avec les settings
-> Faire le lien avec les douze facteurs
-> Construction du fichier setup.cfg
NOTE: vérifier s'il s'agit bien d'une forme de convention :-p
NOTE: Vérifier aussi comment les applications sont construites. Type DRF, Django Social Auth, tout ça.
=== Structure finale de l'environnement
=== Structure finale de notre environnement
Nous avons donc la strucutre finale pour notre environnement de travail:
[source,bash]
----
$ (gwift) fred@aerys:~/Sources/gwift$ tree gwift
gwift
├── docs
│   └── README.md
(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── gwift
│   ├── asgi.py
│   ├── __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
├── Makefile
├── manage.py
├── README.md
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini
3 directories, 13 files
----
===
=== Cookie cutter
* Créez systématiquement un environnement virtuel pour chaque projet sur lequel vous travaillez
* La description des dépendances utilisées pour un projet doivent faire partie intégrante des sources
C'est ici que le projet http://cookiecutter.readthedocs.io/en/latest/readme.html[CookieCutter] va être intéressant: les X premières étapes peuvent être *bypassées* par une simple commande.

View File

@ -41,14 +41,13 @@ $ pep8 . --statistics -qq
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez `pyflakes`: cette librairie analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
Il existe une solution qui couvre ces deux domaines: https://github.com/PyCQA/flake8[flake8].
Sur base de la même interface que `pep8`, vous rencontreez en plus tous les avantages liés à `pyflakes`
==== PEP257 - Docstring Conventions
Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires, de documenter son code.
Cela impose une certaine rigueur, mais améliore énormément la qualité (et la reprise) du code par une tierce personne. Cela implique aussi de **tout** documenter: les modules, les paquets, les classes, les fonctions, méthodes, ... Tout doit avoir un *docstring* associé :-).
Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code.
Cela impose une certaine rigueur, mais améliore énormément la qualité (et la reprise) du code par une tierce personne.
Cela implique aussi de **tout** documenter: les modules, les paquets, les classes, les fonctions, méthodes, ...
Tout doit avoir un *docstring* associé :-).
WARNING: Documentation: be obsessed!
@ -113,6 +112,287 @@ Pour ceux que cela pourrait intéresser, il existe https://marketplace.visualstu
.autodocstring
image::images/environment/python-docstring-vscode.png[]
NOTE: Nous le verrons plus loin, Django permet de rendre la documentation immédiatement accessible depuis son interface d'administration.
==== Linters
Il existe plusieurs niveaux de _linters_:
. Le premier niveau concerne https://pypi.org/project/pycodestyle/[pycodestyle] (anciennement, `pep8` justement...), qui analyse votre code à la recherche d'erreurs de convention.
. Le deuxième niveau concerne https://pypi.org/project/pyflakes/[pyflakes]. Pyflakes est un _simple_ footnote:[Ce n'est pas moi qui le dit, c'est la doc du projet] programme qui recherchera des erreurs parmi vos fichiers Python.
. Le troisième niveau est https://pypi.org/project/flake8/[Flake8], qui regroupe les deux premiers niveaux, en plus d'y ajouter flexibilité, extensions et une analyse de complexité de McCabe.
. Le quatrième niveau footnote:[Oui, en Python, il n'y a que quatre cercles à l'Enfer] est https://pylint.org/[PyLint].
PyLint est le meilleur ami de votre _moi_ futur, un peu comme quand vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire le lendemain: il rendra votre code soyeux et brillant, en posant des affirmations spécifiques.
A vous de les traiter en corrigeant le code ou en apposant un _tag_ indiquant que vous avez pris connaissance de la remarque, que vous en avez tenu compte, et que vous choisissez malgré tout de faire autrement.
Pour vous donner une idée, voici ce que cela pourrait donner avec un code pas très propre et qui ne sert à rien:
[source,python]
----
from datetime import datetime
"""On stocke la date du jour dans la variable ToD4y"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)
----
Avec Flake8, nous obtiendrons ceci:
[source,bash]
----
test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
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 d'espace après un opérateur, une ligne vide à la fin du fichier, ... Ces _erreurs_ n'en 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).
L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant:
[source,text]
----
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10
----
En gros, j'ai programmé comme une grosse bouse anémique (et oui, le score d'évaluation du code permet bien d'aller en négatif). En vrac, on trouve des problèmes liés:
* au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
* à des variables non définies (E0602)
* de la documentation manquante (C0114, C0116)
* de la redéfinition de variables (W0621).
Pour reprendre la http://pylint.pycqa.org/en/latest/user_guide/message-control.html[documentation], chaque code possède sa signification (ouf!):
* C convention related checks
* R refactoring related checks
* W various warnings
* E errors, for probable bugs in the code
* F fatal, if an error occurred which prevented pylint from doing further* processing.
TODO: Expliquer comment faire pour tagger une explication.
==== Formatage de code
Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de style de rédaction pour la documentation (PEP257), d'un _linter_ pour nous indiquer quels morceaux de code doivent absolument être revus, ...
Reste que ces tâches sont [line-through]#parfois# (très) souvent fastidieuses: écrire un code propre et systématiquement cohérent est une tâche ardue. Heureusement, il existe des outils pour nous aider (un peu).
A nouveau, il existe plusieurs possibilités de formatage automatique du code.
Même si elle n'est pas parfaite, https://black.readthedocs.io/en/stable/[Black] arrive à un compromis entre la clarté du code, la facilité d'installation et d'intégration et un résultat.
Est-ce que ce formatage est idéal et accepté par tout le monde ?
Non. Même Pylint arrivera parfois à râler.
Mais ce formatage conviendra dans 97,83% des cas (au moins).
> By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.
>
> Black makes code review faster by producing the smallest diffs possible. Blackened code looks the same regardless of the project youre reading. Formatting becomes transparent after a while and you can focus on the 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é_". 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_".
==== Complexité cyclomatique
A nouveau, un greffon pour `flake8` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre `--max-complexity`. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
==== Typage statique
-> Mypy
==== Tests unitaires
*-> PyTest*
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe `TestCase` depuis le package `django.test`:
[source,python]
----
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
----
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
TODO: Vérifier comment les applications sont construites. Type DRF, Django Social Auth, tout ça.
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet https://pypi.python.org/pypi/coverage/[coverage], qui se charge d'évaluer le pourcentage de code couvert par les tests. 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 l'exécution.
[source,bash]
----
# requirements/base.text
[...]
coverage
django_coverage_plugin
----
[source,bash]
----
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
----
[source,bash]
----
$ 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
----
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes.
==== Matrice de compatibilité
Décrire un fichier tox.ini
[source,bash]
----
$ touch tox.ini
----
==== Configuration globale
Décrire le fichier setup.cfg
[source,bash]
----
$ touch setup.cfg
----
==== Makefile
Pour gagner un peu de temps, n'hésitez pas à créer un fichier `Makefile` que vous placerez à la racine du projet. L'exemple ci-dessous permettra, grâce à la commande `make coverage`, d'arriver au même résultat que ci-dessus:
[source,makefile]
----
# 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."
----
==== The Zen of Python
[source,python]
----
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
----
=== Environnement de développement
Concrètement, nous pourrions tout à fait nous limiter à Notepad ou Notepad++.

View File

@ -5,3 +5,5 @@ include::12-factors.adoc[]
include::maintainable-applications.adoc[]
include::solid.adoc[]
include::mccabe.adoc[]

View File

@ -0,0 +1,58 @@
=== Complexité de McCabe
La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section.
Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite.
Par exemple:
[source,python]
----
if True == False:
pass # never happens
# continue ...
----
TODO: faut vraiment reprendre un cas un peu plus lisible. Là, c'est naze.
La condition existe, mais nous ne passerons jamais dedans.
A l'inverse, le code suivant aura une complexité moisie à cause du nombre de conditions imbriquées:
[source,python]
----
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
----
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de cinq:
. le cas par défaut (a est différent de b, rien ne se passe),
. le cas où `a` est égal à `b`, mais où `b` est différent de `c`
. le cas où `a` est égal à `b`, `b` est égal à `c`, mais `c` est différent de `d`
. le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d`, mais `d` est différent de `e`
. le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d` et `d` est égal à `e`
La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1.
Si nous rencontrons une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, nous allons devoir vérifier au moins chacun des cas pour nous assurer que la couverture est complète.
Nous devrions donc trouver:
. Un test où rien de se passe (`a != b`)
. Un test pour entrer dans la condition `a == b`
. Un test pour entrer dans la condition `b == c`
. Un test pour entrer dans la condition `c == d`
. Un test pour entrer dans la condition `d == e`
Nous avons donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc.
Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
NOTE: A noter que refactoriser un bloc pour en extraire une méthode n'améliorera pas la complexité cyclomatique globale de l'application. Mais nous visons ici une amélioration *locale*.

View File

@ -1,6 +0,0 @@
== En résumé
* Créez systématiquement un environnement virtuel pour chaque projet sur lequel vous travaillez
* La description des dépendances utilisées pour un projet doivent faire partie intégrante des sources
C'est ici que le projet http://cookiecutter.readthedocs.io/en/latest/readme.html[CookieCutter] va être intéressant: les X premières étapes peuvent être *bypassées* par une simple commande.

View File

@ -1,298 +0,0 @@
== Chaîne d'outils
=== Tests
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe `TestCase` depuis le package `django.test`:
[source,python]
----
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
----
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet https://pypi.python.org/pypi/coverage/[coverage], qui se charge d'évaluer le pourcentage de code couvert par les tests. 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 l'exécution.
[source,bash]
----
# requirements/base.text
[...]
coverage
django_coverage_plugin
----
[source,bash]
----
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
----
NOTE: le bloc ci-dessous est à revoir pour isoler la configuration.
[source,bash]
----
$ 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
----
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, n'hésitez pas à créer un fichier `Makefile` que vous placerez à la racine du projet. L'exemple ci-dessous permettra, grâce à la commande `make coverage`, d'arriver au même résultat que ci-dessus:
[source,makefile]
----
# 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."
----
==== Complexité de McCabe
La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
[source,python]
----
if True == True:
pass # never happens
# continue ...
----
La condition existe, mais on ne passera jamais dedans. A l'inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:
[source,python]
----
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
----
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu'à arriver à l'impression à l'écran et à la valeur de retour. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s'assurer que la couverture est complète. On devrait donc trouver:
. Un test pour entrer dans la condition `a == b`
. Un test pour entrer dans la condition `b == c`
. Un test pour entrer dans la condition `c == d`
. Un test pour entrer dans la condition `d == e`
. Et s'assurer que n'importe quel autre cas retournera la valeur `None`.
On a donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
NOTE: Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
A nouveau, un greffon pour `flake8` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre `--max-complexity`. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
==== Documentation
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont http://sphinx-doc.org/[Sphinx] et http://www.mkdocs.org/[MkDocs]. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des https://duckduckgo.com/?q=documentation+driven+development&t=ffsb[tests orientés documentation]. A contrario, votre syntaxe devra respecter https://en.wikipedia.org/wiki/ReStructuredText[ReStructuredText]. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans ses résultats.
NOTE: parler aussi d'asciidoctor (même si moins bien intégré).
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de `Flake8`, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
NOTE: voir si il ne faudrait pas mieux passer par pydocstyle.
[source,bash]
----
$ pip install flake8_docstrings
----
Lancez ensuite `flake8` avec la commande `flake8 . --exclude="migrations"`. Sur notre projet (presque) vide, le résultat sera le suivant:
[source,bash]
----
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
----
Bref, on le voit: nous n'avons que très peu de modules, et aucun d'eux n'est commenté.
En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface d'administration.
=== pep8, flake8, pylint
Un outil existe et listera lensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour linstaller, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom dun répertoire, le nom dun fichier .py, ...).
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez pyflakes: il analaysera vos sources à la recherche derreurs (imports inutilsés, méthodes inconnues, etc.).
Et finalement, si vous voulez grouper les deux, il existe flake8. Sur base la même interface que pep8, vous aurez en plus des avertissements concernant le code source.
[source,python]
--
from datetime import datetime
"""On stocke la date du jour dans la variable ToD4y"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)
--
NOTE: l'exemple est sans doute un peu trop tiré par les cheveux...
L'exécution de la commande flake8 . retourne ceci:
[source,bash]
--
test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file
--
On trouve des erreurs:
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre d'espace après un opérateur, une ligne vide à la fin du fichier, ... Ces _erreurs_ n'en 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.
L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant:
[source,text]
----
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10
----
En gros, j'ai programmé comme une grosse bouse anémique (et oui, le score d'évaluation du code permet bien d'aller en négatif). En vrac, on trouve des problèmes liés:
* au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
* à des variables non définies (E0602)
* de la documentation manquante (C0114, C0116)
* de la redéfinition de variables (W0621).
Pour reprendre la http://pylint.pycqa.org/en/latest/user_guide/message-control.html[documentation], chaque code possède sa signification (ouf!):
* C convention related checks
* R refactoring related checks
* W various warnings
* E errors, for probable bugs in the code
* F fatal, if an error occurred which prevented pylint from doing further* processing.
PyLint est la version **++**, pour ceux qui veulent un code propre et sans bavure.
=== Black
By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.
Black makes code review faster by producing the smallest diffs possible. Blackened code looks the same regardless of the project youre reading. Formatting becomes transparent after a while and you can focus on the content instead.
https://black.readthedocs.io/en/stable/[Black].
Une chose qui fonctionne bien avec le langage Go, c'est que les outils de base sont intégrés au compilateur : le formatage de code et les tests unitaires sont à la portée de tout le monde au travers de deux commandes simples :
. `go fmt`
. `go test`
En Python, c'est plus complexe que cela, puisqu'il n'existe pas une manière unique d'arriver à un résultat (on l'a vu ci-dessus, rien que pour les tests, on a au moins deux librairies...).
Pour revenir à Go : est-ce que ce formatage est idéal et accepté par tout le monde ? Non.
Black fait le même travail: il arrive à un compromis entre la clarté du code, la facilité d'installation et d'intégration et un résultat. Ce résultat ne sera pas parfait, mais il conviendra dans 97,83% des cas (au moins).
=== pytest
=== mypy
=== Towncrier
voir https://pypi.org/project/towncrier/[ici]

View File

@ -1,73 +0,0 @@
== Travailler en isolation
=== Création de l'environnement virtuel
Commencons par créer un environnement virtuel, afin d'y stocker les dépendances. Placez-vous dans le répertoire dans lequel vous pourrez stocker tous vos environnements (ces environnements sont indépendants des sources; ils peuvent donc être placés n'importe où sur votre disque - évitez peut-être juste de les mettre pile dans le même répertoire que votre code source). Lancez ensuite la commande `python3 -m venv gwift-env`.
Ceci créera l'arborescence de fichiers suivante, qui peut à nouveau être un peu différente en fonction du système d'exploitation:
[source,bash]
----
fred@aerys:~/Sources/.venvs/gwift-env$ ls
bin include lib lib64 pyvenv.cfg share
----
Nous pouvons ensuite l'activer grâce à la commande `source gwift-env/bin/activate`.
[source,bash]
----
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$ <1>
----
Pour désactiver l'environnement virtuel, il suffit d'utiliser la commande `deactivate`
=== Installation de Django et création du répertoire de travail
=== Gestion des dépendances
=== Matrice de compatibilité
Décrire un fichier tox.ini
[source,bash]
----
$ touch tox.ini
----
=== Licence
Décrire une licence ? :-)
[source,bash]
----
$ touch LICENCE
----
=== Configuration globale
Décrire le fichier setup.cfg
[source,bash]
----
$ touch setup.cfg
----
=== Makefile
Décrire le makefile :)
[source,bash]
----
$ touch Makefile
----

View File

@ -180,6 +180,11 @@ Toujours dans une optique de centralisation, les migrations sont directement emb
A noter que les migrations n'appliqueront de modifications que si le schéma est impacté. Ajouter une propriété `related_name` sur une ForeignKey n'engendrera aucune nouvelle action de migration, puisque ce type d'action ne s'applique que sur l'ORM, et pas directement sur la base de données: au niveau des tables, rien ne change. Seul le code et le modèle sont impactés.
https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html[reset migrations].
> En gros, soit on supprime toutes les migrations (en conservant le fichier __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 s'y conforment... Ce qui n'est pas gagné.
=== Shell