Génie logiciel : coder facilement, coder à plusieurs, qualité du code, gestion de projets

In [1]:
from IPython.display import HTML, Image
HTML('''<script>
code_show=true; 
function code_toggle() {if (code_show){$('div.input').hide();} else {$('div.input').show();}code_show = !code_show} 
$( document ).ready(code_toggle);
</script>
<form action="javascript:code_toggle()"><input type="submit" 
value="Cliquer ici pour cacher / afficher le code brut."></form>''')
Out[1]:

François HU - 01/10/19 - séance 5

In [5]:
from jyquickhelper import add_notebook_menu
add_notebook_menu()
Out[5]:
run previous cell, wait for 2 seconds

Robustesse du code

Tests unitaires (unittest, pytest)

faire des tests (dans un programme test_*.py ou *_test.py par convention) pour localiser la source d’erreur dans un programme (exemple de package : unittest, pytest)

In [78]:
Image("img/unittest.png", width=700, height = 200)
Out[78]:
  • exemple de test unitaire avec unittest : on veut calculer l'aire d'un cercle
In [7]:
from math import pi

def circle_area(r):
    return(pi*(r**2))
In [8]:
# test fonction
message = "Aire du cercle de rayon r = {rayon} est {aire}"
for r in [2, -3, 2+5j, True, "radius"]:
    A = circle_area(r)
    print(message.format(rayon = r, aire = A))
Aire du cercle de rayon r = 2 est 12.566370614359172
Aire du cercle de rayon r = -3 est 28.274333882308138
Aire du cercle de rayon r = (2+5j) est (-65.97344572538566+62.83185307179586j)
Aire du cercle de rayon r = True est 3.141592653589793
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-c0d4d58d2528> in <module>
      2 message = "Aire du cercle de rayon r = {rayon} est {aire}"
      3 for r in [2, -3, 2+5j, True, "radius"]:
----> 4     A = circle_area(r)
      5     print(message.format(rayon = r, aire = A))

<ipython-input-7-886f03e67d62> in circle_area(r)
      2 
      3 def circle_area(r):
----> 4     return(pi*(r**2))

TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
In [23]:
import unittest

class TestCircleArea(unittest.TestCase):
    def test_area(self):
        # test aires lorsque rayon >= 0
        self.assertAlmostEqual(circle_area(1), pi)
        self.assertAlmostEqual(circle_area(0), 0)
        self.assertAlmostEqual(circle_area(1.1), pi * 1.1**2)
In [28]:
! python -m unittest
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
In [26]:
class TestCircleArea(unittest.TestCase):
    def test_area(self):
        # test aires lorsque rayon >= 0
        self.assertAlmostEqual(circle_area(1), pi)
        self.assertAlmostEqual(circle_area(0), 0)
        self.assertAlmostEqual(circle_area(1.1), pi * 1.1**2)
        
    def test_values(self):
        # test aires lorsque rayon <0
        self.assertRaises(ValueError, circle_area, -2)
        
    def test_types(self):
        # test aires lorsque rayon n'est pas un 'int'
        self.assertRaises(ValueError, circle_area, 3+5j)
        self.assertRaises(ValueError, circle_area, True)
        self.assertRaises(ValueError, circle_area, "radius")
In [29]:
#unittest.main(argv=[''], verbosity=2, exit=False)
! python -m unittest
.FF
======================================================================
FAIL: test_types (test_my_program.TestCircleArea)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/Faugon/Documents/3_enseignements/algo_programmation/cours_5/test_my_program.py", line 17, in test_types
    self.assertRaises(ValueError, circle_area, 3+5j)
AssertionError: ValueError not raised by circle_area

======================================================================
FAIL: test_values (test_my_program.TestCircleArea)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/Faugon/Documents/3_enseignements/algo_programmation/cours_5/test_my_program.py", line 13, in test_values
    self.assertRaises(ValueError, circle_area, -2)
AssertionError: ValueError not raised by circle_area

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=2)
In [13]:
from math import pi

def circle_area(r):
    if type(r) not in [int, float]:
        raise ValueError("le rayon doit etre un entier positif")
    if r < 0:
        raise ValueError("le rayon ne peut pas etre negatif")
    return(pi*(r**2))
In [34]:
#unittest.main(argv=[''], verbosity=2, exit=False)
! python -m unittest
----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
  • avec pytest
In [ ]:
import pytest

def test_area():
    # test aires lorsque rayon >= 0
    circle_area(1) == pi
    circle_area(0) == 0
    circle_area(1.1) == pi * 1.1**2

def test_values():
    # test aires lorsque rayon <0
    with pytest.raises(ValueError):
        circle_area(-2)
    
def test_types():
    # test aires lorsque rayon n'est pas un 'int'
    with pytest.raises(ValueError):
        circle_area(3+5j)
        circle_area(True)
        circle_area("radius")
In [36]:
! py.test test_my_program.py
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.2.1, py-1.8.0, pluggy-0.13.0
rootdir: /Users/Faugon/Documents/3_enseignements/algo_programmation/cours_5
collected 3 items                                                              

test_my_program.py ...                                                   [100%]

============================== 3 passed in 0.01s ===============================

ou

In [35]:
! py.test
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.2.1, py-1.8.0, pluggy-0.13.0
rootdir: /Users/Faugon/Documents/3_enseignements/algo_programmation/cours_5
collected 3 items                                                              

test_my_program.py ...                                                   [100%]

============================== 3 passed in 0.06s ===============================

Mock

désigne une astuce uilisée pour écrire un test unitaire pour une fonctionnalité utilisant une ressource externe comme internet (exple de package : unittest.mock, **)

In [79]:
Image("img/mocking.png", width=700, height = 200)
Out[79]:

Couverture des tests unitaires

mesure le nombre de lignes de code effectivement executée lors de l’exécution des tests unitaires. (objectif env. 80%. exple : module coverage pour mesurer la couverture)

In [12]:
Image("img/coverage.png", width=600, height = 200)
Out[12]:

Facile d’augmenter la couverture pour des fonctions de calculs numériques

Difficile pour un processus d’automatisation qui requiert l’accès à une ressource externe

  • exemple de couverture :

au lieu de faire :

$ python test_my_program.py arg1 arg2

on fait :

$ coverage run test_my_program.py arg1 arg2

ensuite :

$ coverage report

ou :

$ coverage report -m

l'option "-m" identifie la ou les lignes posant problème

pour une présentation plus propre :

coverage html

voici notre programme et notre programme-test :

In [15]:
Image("img/my_program.png", width=650, height = 200)
Out[15]:
In [5]:
! coverage report test_my_program.py
Name                 Stmts   Miss  Cover
----------------------------------------
test_my_program.py      15      0   100%

Profiling

Permet de mesurer la vitesse d’exécution et la consommation de chaque fonction ou chaque ligne. Quand est-ce qu'on utilise un profiling ? le programme est lent et on veut savoir pourquoi.

In [21]:
Image("img/profiling.png", width=580, height = 200)
Out[21]:
In [1]:
from IPython.display import IFrame
IFrame("http://www.xavierdupre.fr/app/ensae_teaching_cs/helpsphinx3/notebooks/profiling_example.html#profilingexamplerst", 
       width=920, height=550)
Out[1]:

Autres types de robustesse du code

Déploiement

Une application (web p.ex.) est souvent déployée sur plusieurs machines. Une entreprise dispose en générale de plusieurs environnements de test afin de pouvoir tester une application ou un service en grandeur réelle sans l’exposer aux utilisateurs. (exple : voir ansible, automatisation facile et rapide)

Tests logiques et preuve formelle

Tous les tests présentés ci-dessus ne font que tester un cas particulier. La fonction marche avec ce jeu de données dans ce cas précis. Le test ne vérifie pas que la fonction est valide dans tous les cas. Cela ne peut passer que part une preuve formelle que le code fonctionne. Dans certains cas, cette preuve formelle est nécessaire car toute erreur a un coût prohibitif. Les logiciels de calculs utilisée par Ariane Espace le sont. On rentre dans le domaine des mathématiques

Pratiques utiles

Logging

Logger consiste à enregister des événements, des opérations effectués par un programme dans le but de les analyser plus tard en cas d’erreur.

In [11]:
Image("img/logging1.png", width=500, height = 200)
Out[11]:
In [107]:
Image("img/logging2.png", width=700, height = 200)
Out[107]:
In [1]:
import logging
from math import sqrt

# creer et configurer notre logger
LOG_FORMAT = "%(levelname)s %(asctime)s - %(message)s"
logging.basicConfig(filename = "process.log", level = logging.DEBUG, format = LOG_FORMAT, filemode = "w")
logger = logging.getLogger()

# tester le logger
def quadratic_formula(a,b,c):
    """Retourne la solution de l'equation ax^2 + bx + c = 0"""
    logger.info("quadratic_formula({0},{1},{2})".format(a,b,c))
    
    # calcul du discriminant
    disc = b**2 - 4*a*c
    
    # calcul des 2 racines
    logger.debug("# calcul des 2 racines")
    root1 = (-b + sqrt(disc)) / (2*a)
    root2 = (-b - sqrt(disc)) / (2*a)
    
    # retourne les racines
    logger.debug("# retourne les racines")
    
    return(root1, root2)
In [104]:
roots = quadratic_formula(1,0,-4)
print(roots)
(2.0, -2.0)
In [110]:
Image("img/exemple_logging1.png", width=700, height = 200)
Out[110]:
In [2]:
roots = quadratic_formula(1,0,1)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-41dffaa824fe> in <module>
----> 1 roots = quadratic_formula(1,0,1)

<ipython-input-1-090c10bbe367> in quadratic_formula(a, b, c)
     17     # calcul des 2 racines
     18     logger.debug("# calcul des 2 racines")
---> 19     root1 = (-b + sqrt(disc)) / (2*a)
     20     root2 = (-b - sqrt(disc)) / (2*a)
     21 

ValueError: math domain error
In [5]:
Image("img/exemple_logging2.png", width=700, height = 200)
Out[5]:

niveaux de logging : Debug, Info, Warning, Error, Critical

Documentation

L’outil standard en Python est Sphinx. Il aboutit à une documentation statique HTML.

Style

Annotations

Le langage Python est faiblement typé. On précise parfois le type attendu dans la documentation d’une fonction ou d’une classe. Les annotations sont un moyen formel de le faire. Même si elle n’a aucune incidence, cette information peut être utilisée

In [7]:
def foo(prefix, suffix):
    """
    prefix: premier mot
    suffix: dernier mot
    
    returns : les deux mots avec "et" entre les deux
    """
    return prefix + " et " + suffix

res = foo("manger", "boire")
print(res)
manger et boire
In [9]:
help(foo)
Help on function foo in module __main__:

foo(prefix, suffix)
    prefix: premier mot
    suffix: dernier mot
    
    retourne : les deux mots avec "et" entre les deux

cependant on nous donne que seulement un string

In [14]:
def foo(prefix: "premier mot",
        suffix: "dernier mot") -> "les deux mots avec 'et' entre les deux":
    return prefix + " et " + suffix
res = foo("manger", "boire")
print(res)
print()
help(foo)
manger et boire

Help on function foo in module __main__:

foo(prefix: 'premier mot', suffix: 'dernier mot') -> "les deux mots avec 'et' entre les deux"

In [21]:
from pprint import pprint
pprint(foo.__annotations__)
{'prefix': 'premier mot',
 'return': "les deux mots avec 'et' entre les deux",
 'suffix': 'dernier mot'}
In [22]:
def foo(prefix: str,
        suffix: str) -> str:
    return prefix + " et " + suffix
res = foo("manger", "boire")
print(res)
print()
help(foo)
manger et boire

Help on function foo in module __main__:

foo(prefix: str, suffix: str) -> str

PEP8

Il est plus facile de lire un code qui suit toujours les mêmes règles d’écriture. En Python, on les appelles PEP8. Ce ne sont pas des règles à suivre à la lettre mais la plupart des développeurs python les suivent. Quelques conventions :

  • ne pas dépasser 79 caractères par ligne de code ...

  • sauter deux lignes entre les fonctions et les classes (mais une ligne entre les méthodes d'une classe)

  • variables_regulieres : en minuscule avec des "_" si besoin

  • CONSTANTES : tout en majuscule

  • nom_fonction() : en minuscule avec des "_" si besoin

  • ClassNames : Majuscule pour chaque début de mot

  • _les_proprietes_non_publics : "_" au debut

  • conflitnom : "_" a la fin si il y a des conflits de nom

Design

En terme de design, scikit-learn est un modèle du genre. La librairie propose des algorithmes de machine learning - ce n’est pas nouveau - mais son adoption rapide est due à la simplicité de son interface, de son design.

Outils

Logiciel de suivi de source

C’est devenu un outil incontournable pour garder la trace des modifications apporter à un programme. La première tâche est la possiblité de revenir en arrière. Aujourd’hui, la plupart des nouveaux projets commencent sur git. Voici les trois sites suivants qui hébergent gratuitement les projets open source :

  • GitHub

  • GitLab

  • Bitbucket

Gestion de projet

Le terme consacré est KanBan. Il est issu des méthodes agiles. Concrètement, plus on en nombreux à travailler sur le même projet, plus il est difficile de garder la trace de ce que chacun fait. Cette approche a été en quelque sorte validée par la pratique.

Revue de code

Une revue de code intervient avant la mise à jour du code d’un logiciel. C’est l’occasion pour un dévelopeur de partager ses modifications avec le reste de son équipe qui commentent les parties du code qui leur déplaisent ou qu’ils approuvent si la mise à jour leur convient. La règle est souvent qu’une modification ne peut être prise en compte dans le code de l’application que si un ou deux autres dévelopeurs la valide.

Poussé à l’extrême, cela devient le pair programming qui consiste à programmer à deux devant le même écran. C’est assez cauchemardesque si c’est permanent. En pratique, la revue de code est un exercice utile. Un logiciel open source : gerrit.

Continuous integration (intégration continue)

On devrait faire tourner l’ensemble des tests unitaires à chaque modification. En pratique, on le fait pas toujours voire rarement car cela prend trop de temps. On délègue cette tâche à une machine distante (voir les projets open source travis sur Linux, Circle CI, appveyor sur Windows. Localement, on peut utiliser Jenkins qui est très simple d’utilisation ou BuildBot).