== Chaîne d'outils

Le langage Python fonctionne avec un système d’améliorations basées sur des propositions: les PEP, ou “Python Enhancement Proposal”. 

Celle qui va nous intéresser pour cette section est la https://www.python.org/dev/peps/pep-0008/[PEP 8 -- Style Guide for Python Code]. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour l’indentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que d’autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.

=== PEP8

Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le `Benevolent Dictator For Life <http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life>`_.

La PEP qui nous intéresse plus particulièrement pour la suite est la `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_, ou "Style Guide for Python Code". Elle spécifie des conventions d'organisation et de formatage de code Python, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, etc. En bref, elle décrit comment écrire du code proprement pour que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.

Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (``.``, le nom d'un répertoire, le nom d'un fichier ``.py``, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options ``--statistics -qq``.

.. code-block:: shell

    $ pep8 . --statistics -qq

    7       E101 indentation contains mixed spaces and tabs
    6       E122 continuation line missing indentation or outdented
    8       E127 continuation line over-indented for visual indent
    23      E128 continuation line under-indented for visual indent
    3       E131 continuation line unaligned for hanging indent
    12      E201 whitespace after '{'
    13      E202 whitespace before '}'
    86      E203 whitespace before ':'

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 analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).

Finalement, la solution qui couvre ces deux domaines existe et s'intitule `flake8 <https://github.com/PyCQA/flake8>`_. Sur base la même interface que ``pep8``, vous aurez en plus tous les avantages liés à ``pyflakes`` concernant votre code source.

==== PEP257

.. todo:: à remplir avec ``pydocstyle``.

==== 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``:

.. code-block:: 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 `coverage <https://pypi.python.org/pypi/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.

.. code-block:: shell

    # requirements/base.text
    [...]
    coverage
    django_coverage_plugin

.. code-block:: shell

    # .coveragerc to control coverage.py
    [run]
    branch = True
    omit = ../*migrations*
    plugins = 
        django_coverage_plugin

    [report]
    ignore_errors = True

    [html]
    directory = coverage_html_report
    

.. todo:: le bloc ci-dessous est à revoir pour isoler la configuration.

.. code-block:: shell

    $ 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:

.. code-block:: shell

    # 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 `complexité cyclomatique <https://fr.wikipedia.org/wiki/Nombre_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:

.. code-block:: 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:

.. code-block:: 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:

 1. Un test pour entrer (ou non) dans la condition ``a == b``
 2. Un test pour entrer (ou non) dans la condition ``b == c``
 3. Un test pour entrer (ou non) dans la condition ``c == d``
 4. Un test pour entrer (ou non) dans la condition ``d == e``
 5. 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 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 `Sphinx <http://sphinx-doc.org/>`_ et `MkDocs <http://www.mkdocs.org/>`_. 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 `tests orientés documentation <https://duckduckgo.com/?q=documentation+driven+development&t=ffsb>`_. A contrario, votre syntaxe devra respecter `ReStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans son résultat.

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.

.. code-block:: shell

    $ pip install flake8_docstrings

Lancez ensuite `flake8` avec la commande ``flake8 . --exclude="migrations"``. Sur notre projet (presque) vide, le résultat sera le suivant:

.. code-block:: shell

    $ 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 l’ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l’installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom d’un répertoire, le nom d’un 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 d’erreurs (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)


--

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


=== pytest


=== mypy

