== Le langage Python

Le langage https://www.python.org/[Python] est un https://docs.python.org/3/faq/general.html#what-is-python[langage de programmation] interprété, interactif, amusant, orienté objet (souvent), fonctionnel (parfois), open source, multi-plateformes, flexible, facile à apprendre et difficile à maîtriser.

.https://xkcd.com/353/
image::images/xkcd-353-python.png[]

A première vue, et suivants les autres langages que vous connaitriez ou auriez déjà abordé, certains concepts restent difficiles à aborder: l'indentation définit l'étendue d'un bloc (classe, fonction, méthode, boucle, condition, ...), il n'y a pas de typage fort des variables et le compilateur n'est pas là pour assurer le filet de sécurité avant la mise en production (puisqu'il n'y a pas de compilateur 😛).
Et malgré ces quelques points, Python reste un langage généraliste accessible et "bon partout", et de pouvoir se reposer sur un écosystème stable et fonctionnel.

Il 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 http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life[Benevolent Dictator For Life].

NOTE: Le langage Python utilise un typage dynamique appelé https://fr.wikipedia.org/wiki/Duck_typing[*duck typing*]: "_When I see a bird that quacks like a duck, walks like a duck, has feathers and webbed feet and associates with ducks — I’m certainly going to assume that he is a duck_"
Source: http://en.wikipedia.org/wiki/Duck_test[Wikipedia].

Les morceaux de code que vous trouverez ci-dessous seront développés pour Python3.9+ et Django 3.2+.
Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.

=== Eléments de langage

En fonction de votre niveau d'apprentissage du langage, plusieurs ressources pourraient vous aider:

* *Pour les débutants*, https://automatetheboringstuff.com/[Automate the Boring Stuff with Python] cite:[boring_stuff], aka. _Practical Programming for Total Beginners_
* *Pour un (gros) niveau au dessus* et pour un état de l'art du langage, nous ne pouvons que vous recommander le livre Expert Python Programming cite:[expert_python], qui aborde énormément d'aspects du langage en détails (mais pas toujours en profondeur): les différents types d'interpréteurs, les éléments de langage avancés, différents outils de productivité, métaprogrammation, optimisation de code, programmation orientée évènements, multithreading et concurrence, tests, ...
A ce jour, c'est le concentré de sujets liés au langage le plus intéressant qui ait pu arriver entre nos mains.

En parallèle, si vous avez besoin d'un aide-mémoire ou d'une liste exhaustive des types et structures de données du langage, référez-vous au lien suivant: https://gto76.github.io/python-cheatsheet/[Python Cheat Sheet].

==== Protocoles de langage

(((dunder)))

Le modèle de données du langage spécifie un ensemble de méthodes qui peuvent être surchargées.
Ces méthodes suivent une convention de nommage et leur nom est toujours encadré par un double tiret souligné; d'où leur nom de "_dunder methods_" ou "_double-underscore methods_".
La méthode la plus couramment utilisée est la méthode `__init__()`, qui permet de surcharger l'initialisation d'une instance de classe.

[source,python]
----
class CustomUserClass:
    def __init__(self, initiatization_argument):
        ...
----

cite:[expert_python(142-144)]

Ces méthodes, utilisées seules ou selon des combinaisons spécifiques, constituent les _protocoles de langage_.
Une liste complètement des _dunder methods_ peut être trouvée dans la section `Data Model` de https://docs.python.org/3/reference/datamodel.html[la documentation du langage Python].

All operators are also exposed as ordinary functions in the operators module.
The documentation of that module gives a good overview of Python operators.
It can be found at https://docs.python.org/3.9/library/operator.html


If we say that an object implements a specific language protocol, it means that it is compatible with a specific part of the Python language syntax.

The following is a table of the most common protocols within the Python language.

Protocol nameMethodsDescriptionCallable protocol__call__()Allows objects to be called with parentheses:instance()Descriptor protocols__set__(), __get__(), and __del__()Allows us to manipulate the attribute access pattern of classes (see the Descriptors section)Container protocol__contains__()Allows us to test whether or not an object contains some value using the in keyword:value in instance

Python in Comparison with Other LanguagesIterable protocol__iter__()Allows objects to be iterated using the forkeyword:for value in instance:    ...Sequence protocol__getitem__(),__len__()Allows objects to be indexed with square bracket syntax and queried for length using a built-in function:item = instance[index]length = len(instance)Each operator available in Python has its own protocol and operator overloading happens by implementing the dunder methods of that protocol. Python provides over 50 overloadable operators that can be divided into five main groups:• Arithmetic operators • In-place assignment operators• Comparison operators• Identity operators• Bitwise operatorsThat's a lot of protocols so we won't discuss all of them here. We will instead take a look at a practical example that will allow you to better understand how to implement operator overloading on your own


The `__add__()` method is responsible for overloading the `+` (plus sign) operator and here it allows us to add
two matrices together.
Only matrices of the same dimensions can be added together.
This is a fairly simple operation that involves adding all matrix elements one by one to form a new matrix.

The `__sub__()` method is responsible for overloading the `–` (minus sign) operator that will be responsible for matrix subtraction.
To subtract two matrices, we use a similar technique as in the – operator:

[source,python]
----
def __sub__(self, other):
    if (len(self.rows) != len(other.rows) or len(self.rows[0]) != len(other.rows[0])):
        raise ValueError("Matrix dimensions don't match")
    return Matrix([[a - b for a, b in zip(a_row, b_row)] for a_row, b_row in zip(self.rows, other.rows)    ])
----

And the following is the last method we add to our class:

[source,python]
----
def __mul__(self, other):
    if not isinstance(other, Matrix):
        raise TypeError(f"Don't know how to multiply {type(other)} with Matrix")

    if len(self.rows[0]) != len(other.rows):
        raise ValueError("Matrix dimensions don't match")

    rows = [[0 for _ in other.rows[0]]  for _ in self.rows]

    for i in range(len (self.rows)):
        for j in range(len (other.rows[0])):
            for k in range(len (other.rows)):
                rows[i][j] += self.rows[i][k] * other.rows[k][j]

    return Matrix(rows)
----

The last overloaded operator is the most complex one.
This is the `*` operator, which is implemented through the `__mul__()` method.
In linear algebra, matrices don't have the same multiplication operation as real numbers.
Two matrices can be multiplied if the first matrix has a number of columns equal to the number of rows of the second matrix.
The result of that operation is a new matrix where each element is a dot product of the corresponding row of the first matrix
and the corresponding column of the second matrix.
Here we've built our own implementation of the matrix to present the idea of operators overloading.
Although Python lacks a built-in type for matrices, you don't need to build them from scratch.
The NumPy package is one of the best Python mathematical packages and among others provides native support for matrix algebra.
You can easily obtain the NumPy package from PyPI


En fait, l'intérêt concerne surtout la représentation de nos modèles, puisque chaque classe du modèle est représentée par
la définition d'un objet Python.
Nous pouvons donc utiliser ces mêmes *dunder methods* (*double-underscores methods*) pour étoffer les protocoles du langage.

=== The Zen of Python

[source,python]
----
>>> import this
----

[source,text]
----
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!
----


=== PEP8 - Style Guide for Python Code

La première PEP qui va nous intéresser 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, afin 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é.

Dans cet objectif, 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`.

[source,bash]
----
$ 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`: cette librairie analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).


=== PEP257 - Docstring Conventions

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é, la compréhension 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, ...
Ce qui peut également aller à contrecourant d'autres pratiques cite:[clean_code(53-74)]; il y a une juste mesure à prendre entre "tout documenter" et "tout bien documenter":

* Inutile d'ajouter des watermarks, auteurs, ... Git ou tout VCS s'en sortira très bien et sera beaucoup plus efficace que n'importe 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()` d'une personne n'aura pas beaucoup d'intérêt
* S'il est nécessaire de décrire un comportement au sein-même d'une fonction, c'est que ce comportement pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être documentée)

WARNING: Documentation: be obsessed! Mais *le code reste la référence*

Il existe plusieurs types de conventions de documentation:

. PEP 257
. Numpy
. Google Style (parfois connue sous l'intitulé `Napoleon`)
. ...

Les https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings[conventions proposées par Google] nous semblent  plus faciles à lire que du RestructuredText, mais sont parfois moins bien intégrées que les docstrings officiellement supportées (par exemple, https://clize.readthedocs.io/en/stable/[clize] ne reconnait que du RestructuredText; https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/[l'auto-documentation] de Django également).
L'exemple donné dans les guides de style de Google est celui-ci:

[source,python]
----
def fetch_smalltable_rows(table_handle: smalltable.Table,
                          keys: Sequence[Union[bytes, str]],
                          require_all_keys: bool = False,
) -> Mapping[bytes, Tuple[str]]:
    """Fetches rows from a Smalltable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by table_handle.  String keys will be UTF-8 encoded.

    Args:
        table_handle: An open smalltable.Table instance.
        keys: A sequence of strings representing the key of each table
          row to fetch.  String keys will be UTF-8 encoded.
        require_all_keys: Optional; If require_all_keys is True only
          rows with values set for all keys will be returned.

    Returns:
        A dict mapping keys to the corresponding table row data
        fetched. Each row is represented as a tuple of strings. For
        example:

        {b'Serak': ('Rigel VII', 'Preparer'),
         b'Zim': ('Irk', 'Invader'),
         b'Lrrr': ('Omicron Persei 8', 'Emperor')}

        Returned keys are always bytes.  If a key from the keys argument is
        missing from the dictionary, then that row was not found in the
        table (and require_all_keys must have been False).

    Raises:
        IOError: An error occurred accessing the smalltable.
    """
----

C'est-à-dire:

. Une courte ligne d'introduction, descriptive, indiquant ce que la fonction ou la méthode réalise. Attention, la documentation ne doit pas indiquer _comment_ la fonction/méthode est implémentée, mais ce qu'elle fait concrètement (et succintement).
. Une ligne vide
. Une description plus complète et plus verbeuse, si vous le jugez nécessaire
. Une ligne vide
. La description des arguments et paramètres, des valeurs de retour, des exemples et les exceptions qui peuvent être levées.

Un exemple (encore) plus complet peut être trouvé https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html#example-google[dans le dépôt sphinxcontrib-napoleon].
Et ici, nous tombons peut-être dans l'excès de zèle:

[source,python]
----
def module_level_function(param1, param2=None, *args, **kwargs):
    """This is an example of a module level function.

    Function parameters should be documented in the ``Args`` section. The name
    of each parameter is required. The type and description of each parameter
    is optional, but should be included if not obvious.

    If \*args or \*\*kwargs are accepted,
    they should be listed as ``*args`` and ``**kwargs``.

    The format for a parameter is::

        name (type): description
            The description may span multiple lines. Following
            lines should be indented. The "(type)" is optional.

            Multiple paragraphs are supported in parameter
            descriptions.

    Args:
        param1 (int): The first parameter.
        param2 (:obj:`str`, optional): The second parameter. Defaults to None.
            Second line of description should be indented.
        *args: Variable length argument list.
        **kwargs: Arbitrary keyword arguments.

    Returns:
        bool: True if successful, False otherwise.

        The return type is optional and may be specified at the beginning of
        the ``Returns`` section followed by a colon.

        The ``Returns`` section may span multiple lines and paragraphs.
        Following lines should be indented to match the first line.

        The ``Returns`` section supports any reStructuredText formatting,
        including literal blocks::

            {
                'param1': param1,
                'param2': param2
            }

    Raises:
        AttributeError: The ``Raises`` section is a list of all exceptions
            that are relevant to the interface.
        ValueError: If `param2` is equal to `param1`.

    """
    if param1 == param2:
        raise ValueError('param1 may not be equal to param2')
    return True
----

Pour ceux que cela pourrait intéresser, il existe https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring[une extension pour Codium], comme nous le verrons juste après, qui permet de générer automatiquement le squelette de documentation d'un bloc de code:

.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. Toute information pertinente peut donc lier le code à un cas d'utilisation concret.

==== 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, nous trouvons 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.

TODO: Voir si la sortie de pylint est obligatoirement 0 s'il y a un warning

TODO: parler de `pylint --errors-only`


=== 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 clarté du code, facilité d'installation et d'intégration et 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 you’re 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 - https://www.python.org/dev/peps/pep-0585/[PEP585]

Nous vous disions ci-dessus que Python était un langage dynamique interprété.
Concrètement, cela signifie que des erreurs pouvant être détectées à la compilation avec d'autres langages, ne le sont pas avec Python.

Il existe cependant une solution à ce problème, sous la forme de http://mypy-lang.org/[Mypy], qui peut (sous vous le souhaitez ;-)) vérifier une forme de typage statique de votre code source, grâce à une expressivité du code, basée sur des annotations (facultatives, elles aussi).

Ces vérifications se présentent de la manière suivante:

[source,python]
----
from typing import List


def first_int_elem(l: List[int]) -> int:
    return l[0] if l else None


if __name__ == "__main__":
    print(first_int_elem([1, 2, 3]))
    print(first_int_elem(['a', 'b', 'c']))
----

Est-ce que le code ci-dessous fonctionne correctement ?
*Oui*:

[source,bash]
----
λ python mypy-test.py
1
a
----

Malgré que nos annotations déclarent une liste d'entiers, rien ne nous empêche de lui envoyer une liste de caractères, sans que cela ne lui pose de problèmes.

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 l'appelons avec une liste de caractères, alors que nous nous attendions à une liste d'entiers:

[source,bash]
----
λ mypy mypy-test.py
mypy-test.py:7: error: Incompatible return value type (got "Optional[int]", expected "int")
mypy-test.py:12: error: List item 0 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 1 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 2 has incompatible type "str"; expected "int"
Found 4 errors in 1 file (checked 1 source file)
----

Pour corriger ceci, nous devons:

. Importer le type `Optional` et l'utiliser en sortie de notre fonction `first_int_elem`
. Eviter de lui donner de
mauvais paramètres ;-)

[source,python]
----
from typing import List, Optional


def first_int_elem(l: List[int]) -> Optional[int]:
    return l[0] if l else None


if __name__ == "__main__":
    print(first_int_elem([1, 2, 3]))
----

[source,bash]
----
λ mypy mypy-test.py
Success: no issues found in 1 source file
----


==== 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é.

Comme indiqué ci-dessus, Django propose son propre cadre de tests, au travers du package `django.tests`.
Une bonne pratique (parfois discutée) consiste cependant à switcher vers `pytest`, qui présente quelques avantages:

* Une syntaxe plus concise (au prix de https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html[quelques conventions], même si elles restent configurables): un test est une fonction, et ne doit pas obligatoirement faire partie d'une classe héritant de `TestCase` - la seule nécessité étant que cette fonction fasse partie d'un module commençant ou finissant par "test" (`test_example.py` ou `example_test.py`).
* Une compatibilité avec du code Python "classique" - vous ne devrez donc retenir qu'un seul ensemble de commandes ;-)
* Des _fixtures_ faciles à réutiliser entre vos différents composants
* Une compatibilité avec le reste de l'écosystème, dont la couverture de code présentée ci-dessous.

Ainsi, après installation, il nous suffit de créer notre module `test_models.py`, dans lequel nous allons simplement tester l'addition d'un nombre et d'une chaîne de caractères (oui, c'est complètement biesse; on est sur la partie théorique ici):

[source,python]
----
def test_add():
    assert 1 + 1 == "argh"
----

Forcément, cela va planter.
Pour nous en assurer (dès fois que quelqu'un en doute), il nous suffit de démarrer la commande `pytest`:

[source,bash]
----
λ pytest
============================= test session starts ====================================
platform ...
rootdir: ...
plugins: django-4.1.0
collected 1 item

gwift\test_models.py F                                                          [100%]

================================== FAILURES ==========================================
_______________________________ test_basic_add _______________________________________

    def test_basic_add():
>       assert 1 + 1 == "argh"
E       AssertionError: assert (1 + 1) == 'argh'

gwift\test_models.py:2: AssertionError

=========================== short test summary info ==================================
FAILED gwift/test_models.py::test_basic_add - AssertionError: assert (1 + 1) == 'argh'
============================== 1 failed in 0.10s =====================================
----


==== 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.
Le paquet `coverage` se charge d'évaluer le pourcentage de code couvert par les tests.

Avec `pytest`, il convient d'utiliser 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 paquet https://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 l'exécution.

[source,bash]
----
# requirements/base.text
[...]
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
----

<--- / partie obsolète --->

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é

L'intérêt de la matrice de compatibilité consiste à spécifier un ensemble de plusieurs versions d'un même interpréteur (ici, Python), afin de s'assurer que votre application continue à fonctionner. Nous sommes donc un cran plus haut que la spécification des versions des librairies, puisque nous nous situons directement au niveau de l'interpréteur.

L'outil le plus connu est https://tox.readthedocs.io/en/latest/[Tox], qui consiste en un outil basé sur virtualenv et qui permet:

. de vérifier que votre application s'installe correctement avec différentes versions de Python et d'interpréteurs
. de démarrer des tests parmi ces différents environnements

[source,ini]
----
# content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py36,py37,py38,py39
skipsdist = true

[testenv]
deps =
    -r requirements/dev.txt
commands =
    pytest
----

Démarrez ensuite la commande `tox`, pour démarrer la commande `pytest` sur les environnements Python 3.6, 3.7, 3.8 et 3.9, après avoir installé nos dépendances présentes dans le fichier `requirements/dev.txt`.

WARNING: pour que la commande ci-dessus fonctionne correctement, il sera nécessaire que vous ayez les différentes versions d'interpréteurs installées.
Ci-dessus, la commande retournera une erreur pour chaque version non trouvée, avec une erreur type `ERROR:   pyXX: InterpreterNotFound: pythonX.X`.

==== Configuration globale

Décrire le fichier setup.cfg

[source,bash]
----
$ touch setup.cfg
----


==== Dockerfile

[source,docker]
----
# Dockerfile

# Pull base image
#FROM python:3.8
FROM python:3.8-slim-buster

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV DEBIAN_FRONTEND noninteractive
ENV ACCEPT_EULA=Y

# install Microsoft SQL Server requirements.
ENV ACCEPT_EULA=Y
RUN apt-get update -y && apt-get update \
  && apt-get install -y --no-install-recommends curl gcc g++ gnupg


# Add SQL Server ODBC Driver 17
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update \
  && apt-get install -y msodbcsql17 unixodbc-dev

# clean the install.
RUN apt-get -y clean

# Set work directory
WORKDIR /code

# Install dependencies
COPY ./requirements/base.txt /code/requirements/
RUN pip install --upgrade pip
RUN pip install -r ./requirements/base.txt

# Copy project
COPY . /code/

----


==== 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,text]
----
# Makefile for gwift
#

# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
    $(error The 'coverage' command was not found. Make sure you have coverage installed)
endif

.PHONY: help coverage

help:
    @echo "  coverage to run coverage check of the source files."

coverage:
    coverage run --source='.' manage.py test; coverage report; coverage html;
    @echo "Testing of coverage in the sources finished."
----

Pour la petite histoire, `make` peu sembler un peu désuet, mais reste extrêmement efficace.



=== Environnement de développement

Concrètement, nous pourrions tout à fait nous limiter à Notepad ou Notepad++.
Mais à moins d'aimer se fouetter avec un câble USB, nous apprécions la complétion du code, la coloration syntaxique, l'intégration des tests unitaires et d'un debugger, ainsi que deux-trois sucreries qui feront plaisir à n'importe quel développeur.

Si vous manquez d'idées ou si vous ne savez pas par où commencer:

* https://vscodium.com/[VSCodium], avec les plugins https://marketplace.visualstudio.com/items?itemName=ms-python.python[Python]et https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens[GitLens]
* https://www.jetbrains.com/pycharm/[PyCharm]
* https://www.vim.org/[Vim] avec les plugins https://github.com/davidhalter/jedi-vim[Jedi-Vim] et https://github.com/preservim/nerdtree[nerdtree]

Si vous hésitez, et même si Codium n'est 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.

.Codium en action
image::images/environment/codium.png[]

=== Un terminal

_A priori_, les IDE footnote:[Integrated Development Environment] proposés ci-dessus fournissent par défaut ou _via_ des greffons un terminal intégré.
Ceci dit, disposer d'un terminal séparé facilite parfois certaines tâches.

A nouveau, si vous manquez d'idées:

. Si vous êtes sous Windows, téléchargez une copie de https://cmder.net/[Cmder]. Il n'est pas le plus rapide, mais propose une intégration des outils Unix communs (`ls`, `pwd`, `grep`, `ssh`, `git`, ...) sans trop se fouler.
. Pour tout autre système, vous devriez disposer en natif de ce qu'il faut.

.Mise en abîme
image::images/environment/terminal.png[]

=== Un gestionnaire de base de données

Django gère plusieurs moteurs de base de données.
Certains sont gérés nativement par Django (PostgreSQL, MariaDB, SQLite); _a priori_, ces trois-là sont disponibles pour tous les systèmes d'exploitation. D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft SQL Server).

Il n'est pas obligatoire de disposer d'une application de gestion pour ces moteurs: pour les cas d'utilisation simples, le shell Django pourra largement suffire (nous y reviendrons).
Mais pour faciliter la gestion des bases de données elles-même, et si vous n'êtes pas à l'aise avec la ligne de commande, choisissez l'une des applications d'administration ci-dessous en fonction du moteur de base de données que vous souhaitez utiliser.

* Pour *PostgreSQL*, il existe https://www.pgadmin.org/[pgAdmin]
* Pour *MariaDB* ou *MySQL*, partez sur https://www.phpmyadmin.net/[PHPMyAdmin]
* Pour *SQLite*, il existe https://sqlitebrowser.org/[SQLiteBrowser]
PHPMyAdmin ou PgAdmin.


=== Un gestionnaire de mots de passe

Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos applications.
Si vous n'en utilisez pas déjà un, partez sur https://keepassxc.org/[KeepassXC]: il est multi-plateformes, suivi et s'intègre correctement aux différents environnements, tout en restant accessible.

image::images/environment/keepass.png[]


=== Un système de gestion de versions

Il existe plusieurs systèmes de gestion de versions.
Le plus connu à l'heure actuelle est https://git-scm.com/[Git], notamment pour sa (très) grande flexibilité et sa rapidité d'exécution.
Il est une aide précieuse pour développer rapidement des preuves de concept, switcher vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle release à proposer au téléchargement.
Ses deux plus gros défauts concerneraient peut-être sa courbe d'apprentissage pour les nouveaux venus et la complexité des actions qu'il permet de réaliser.

.https://xkcd.com/1597/
image::images/xkcd-1597-git.png[]

Même pour un développeur solitaire, un système de gestion de versions (quel qu'il soit) reste indispensable.

Chaque "*branche*" correspond à une tâche à réaliser: un bogue à corriger (_Hotfix A_), une nouvelle fonctionnalité à ajouter ou un "_truc à essayer_" footnote:[Oui, comme dans "Attends, j'essaie vite un truc, si ça marche, c'est beau."] (_Feature A_ et _Feature B_).

Chaque "*commit*" correspond à une sauvegarde atomique d'un état ou d'un ensemble de modifications cohérentes entre elles.footnote:[Il convient donc de s'abstenir de modifier le CSS d'une application et la couche d'accès à la base de données, sous peine de se faire huer par ses relecteurs au prochain stand-up.]
De cette manière, il est beaucoup plus facile pour le développeur de se concenter sur un sujet en particulier, dans la mesure où celui-ci ne doit pas obligatoirement être clôturé pour appliquer un changement de contexte.

.Git en action
image::images/diagrams/git-workflow.png[]

Cas pratique: vous développez cette nouvelle fonctionnalité qui va révolutionner le monde de demain et d'après-demain, quand, tout à coup (!), vous vous rendez compte que vous avez perdu votre conformité aux normes PCI parce les données des titulaires de cartes ne sont pas isolées correctement.
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`)
. solutionner le problème (sans doute un `;` en trop ?)
. sauver le correctif sur cette branche (`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 l'univers vous réclame à cor et à a cri (`git checkout features/dinolol`)

Finalement, sachez qu'il existe plusieurs manières de gérer ces flux d'informations.
Les plus connus sont https://www.gitflow.com/[Gitflow] et https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/[Threeflow].

==== Décrire ses changements

La description d'un changement se fait _via_ la commande `git commit`.
Il est possible de lui passer directement le message associé à ce changement grâce à l'attribut `-m`, mais c'est une pratique relativement déconseillée: un _commit_ ne doit effectivement pas obligatoirement être décrit sur une seule ligne.
Une description plus complète, accompagnée des éventuels tickets ou références, sera plus complète, plus agréable à lire, et plus facile à revoir pour vos éventuels relecteurs.

De plus, la plupart des plateformes de dépôts présenteront ces informations de manière ergonomique. Par exemple:

.Un exemple de commit affiché dans Gitea
image::images/environment/gitea-commit-message.png[]

La première ligne est reprise comme titre (normalement, sur 50 caractères maximum); le reste est repris comme de la description.

=== Un système de virtualisation

Par "_système de virtualisation_", nous entendons n'importe quel application, système d'exploitation, système de containeurisation, ... qui permette de créer ou recréer un environnement de développement aussi proche que celui en production.
Les solutions sont nombreuses:

* https://www.virtualbox.org/[VirtualBox]
* https://www.vagrantup.com/[Vagrant]
* https://www.docker.com/[Docker]
* https://linuxcontainers.org/lxc/[Linux Containers (LXC)]
* https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v[Hyper-V]

Ces quelques propositions se situent un cran plus loin que la "simple" isolation d'un environnement, puisqu'elles vous permettront de construire un environnement complet.
Elles constituent donc une étape supplémentaires dans la configuration de votre espace de travail, mais en amélioreront la qualité.

Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux solutions automatisables et multiplateformes, dont la configuration peut faire partie intégrante de vos sources.

==== Vagrant

Vagrant consiste en un outil de création et de gestion d'environnements virtualisés, en respectant toujours une même manière de travailler, indépendamment des choix techniques et de l'infrastructure que vous pourriez sélectionner.

> Vagrant is a tool for building and managing virtual machine environments in a single workflow. With an easy-to-use workflow and focus on automation, Vagrant lowers development environment setup time, increases production parity, and makes the "works on my machine" excuse a relic of the past. footnote:[https://www.vagrantup.com/intro]

La partie la plus importante de la configuration de Vagrant pour votre projet consiste à placer un fichier `Vagrantfile` - _a priori_ à la racine de votre projet - et qui contiendra les information suivantes:

* Le choix du _fournisseur_ (*provider*) de virtualisation (Virtualbox, Hyper-V et Docker sont natifs; il est également possible de passer par VMWare, AWS, etc.)
* Une _box_, qui consiste à lui indiquer le type et la version attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et https://app.vagrantup.com/boxes/search[il y a du choix]).
* La manière dont la fourniture (*provisioning*) de l'environnement doit être réalisée: scripts Shell, fichiers, Ansible, Puppet, Chef, ... Choisissez votre favori :-) même s'il est toujours possible de passer par une installation et une maintenance manuelle, après s'être connecté sur la machine.
* Si un espace de stockage doit être partagé entre la machine virtuelle et l'hôte
* Les ports qui doivent être transmis de la machine virtuelle vers l'hôte.

La syntaxe de ce fichier `Vagrantfile` est en https://www.ruby-lang.org/en/[Ruby]. Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exécuté la commande `vagrant init`:

[source,ruby]
----
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|

  config.vm.box = "ubuntu/bionic64"

  config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  config.vm.provider "virtualbox" do |vb|
    vb.gui = true
    vb.memory = "1024"
  end

  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y nginx
  SHELL
end
----

Dans le fichier ci-dessus, nous créons:

* Une nouvelle machine virtuelle (ie. _invitée_) sous Ubuntu Bionic Beaver, en x64
* Avec une correspondance du port `80` de la machine vers le port `8080` de l'hôte, en limitant l'accès à celui-ci - accédez à `localhost:8080` et vous accéderez au port `80` de la machine virtuelle.
* En utilisant Virtualbox comme backend - la mémoire vive allouée sera limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique au démarrage
* Et pour finir, nous voulons appliquer un script de mise à jour `apt-get update` et installer le paquet `nginx`

NOTE: Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichier `Vagrantfile` se trouve) sera synchronisé dans le répertoire `/vagrant` sur la machine invitée.


==== Docker

(copié/collé de cookie-cutter-django)

[source,dockerfile]
----
version: '3'

volumes:
  local_postgres_data: {}
  local_postgres_data_backups: {}

services:
  django: &django
    build:
      context: .
      dockerfile: ./compose/local/django/Dockerfile
    image: khana_local_django
    container_name: django
    depends_on:
      - postgres
    volumes:
      - .:/app:z
    env_file:
      - ./.envs/.local/.django
      - ./.envs/.local/.postgres
    ports:
      - "8000:8000"
    command: /start

  postgres:
    build:
      context: .
      dockerfile: ./compose/production/postgres/Dockerfile
    image: khana_production_postgres
    container_name: postgres
    volumes:
      - local_postgres_data:/var/lib/postgresql/data:Z
      - local_postgres_data_backups:/backups:z
    env_file:
      - ./.envs/.local/.postgres

  docs:
    image: khana_local_docs
    container_name: docs
    build:
      context: .
      dockerfile: ./compose/local/docs/Dockerfile
    env_file:
      - ./.envs/.local/.django
    volumes:
      - ./docs:/docs:z
      - ./config:/app/config:z
      - ./khana:/app/khana:z
    ports:
      - "7000:7000"
    command: /start-docs

  redis:
    image: redis:5.0
    container_name: redis

  celeryworker:
    <<: *django
    image: khana_local_celeryworker
    container_name: celeryworker
    depends_on:
      - redis
      - postgres

    ports: []
    command: /start-celeryworker

  celerybeat:
    <<: *django
    image: khana_local_celerybeat
    container_name: celerybeat
    depends_on:
      - redis
      - postgres

    ports: []
    command: /start-celerybeat

  flower:
    <<: *django
    image: khana_local_flower
    container_name: flower
    ports:
      - "5555:5555"
    command: /start-flower

----

[source,dockerfile]
----
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    command: python /code/manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - 8000:8000
    depends_on:
      - slqserver
  slqserver:
    image: mcr.microsoft.com/mssql/server:2019-latest
    environment:
      - "ACCEPT_EULA=Y"
      - "SA_PASSWORD=sqklgjqihagrtdgqk12§!"
    ports:
      - 1433:1433
    volumes:
      - ../sqlserver/data:/var/opt/mssql/data
      - ../sqlserver/log:/var/opt/mssql/log
      - ../sqlserver/secrets:/var/opt/mssql/secrets

----

[source,dockerfile]
----
FROM python:3.8-slim-buster

ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

RUN apt-get update \
  # dependencies for building Python packages
  && apt-get install -y build-essential \
  # psycopg2 dependencies
  && apt-get install -y libpq-dev \
  # Translations dependencies
  && apt-get install -y gettext \
  # cleaning up unused files
  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
  && rm -rf /var/lib/apt/lists/*

# Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt

COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint

COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start

COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker

COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat

COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower

WORKDIR /app

ENTRYPOINT ["/entrypoint"]
----

NOTE: Voir comment nous pouvons intégrer toutes ces commandes au niveau de la CI et au niveau du déploiement (Docker-compose ?)


==== Base de données

Parfois, SQLite peut être une bonne option:

[quote]
Write througput is the area where SQLite struggles the most, but there's not a ton of compelling data online about how it fares, so I got some of my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly modified1 version of the SQLite kvtest2 program on it. Writing 512 byte blobs as separate transactions, in WAL mode with synchronous=normal3, temp_store=memory, and mmap enabled, I got 13.78μs per write, or ~72,568 writes per second. Going a bit larger, at 32kb writes, I got 303.74μs per write, or ~3,292 writes per second. That's not astronomical, but it's certainly way more than most websites being used by humans need. If you had 10 million daily active users, each one could get more than 600 writes per day with that.

[quote]
Looking at read throughput, SQLite can go pretty far: with the same test above, I got a read throughput of ~496,770 reads/sec (2.013μs/read) for the 512 byte blob. Other people also report similar results — Expensify reports that you can get 4M QPS if you're willing to make some slightly more involved changes and use a beefier server. Four million QPS is enough that every internet user in the world could make ~70 queries per day, with a little headroom left over4. Most websites don't need that kind of throughput. cite:[consider_sqlite]
