.. include:: .. footer:: Cyber-base de la Cité des Sciences et de l\'Industrie à Paris |bullet| 2008-05-17 .. |bullet| unicode:: U+02022 .. |mode| unicode:: U+00D8 .. capital o with stroke =================== Assurance qualité =================== :Auteur: Julien Jehannet :Date: 2008-05-17 :Niveau: Débutant / Confirmé Le code est fait pour être lu : quelques pistes pour améliorer la lisibilité et la maintenabilité de votre code python. .. container:: handout Cette présentation parle de quelques bonnes pratiques destinées à mettre toutes les chances de votre côté pour que la maintenance de vos applications ne devienne pas rapidement un cauchemar. Nous allons au préalable définir rapidement ce qu'est le but de l'assurance qualité; puis nous rappellerons brièvement les principes de bases que tous les développeurs python doivent utiliser. Nous passerons ensuite en revue les outils standards de python pour faciliter la mise au point avant de lister des projets plus ambitieux mais indispensables pour la création de code de qualité. .. contents:: Sommaire :class: handout print Une définition de l'assurance qualité ===================================== Pour satisfaire aux contraintes de qualité et fonctionnalités, l'assurance qualité doit viser un contrôle constant sur : .. class:: incremental - le respect des spécifications et besoins du clients - le développement et la validation de l'application - l'intégration et la production - la livraison des produits techniques et d'une documentation à jour .. container:: handout La maîtrise de l'assurance qualité permet au chef de projet (et a fortiori l'équipe de développement) de proposer un nombre de fonctionnalités croissantes (sans régression) dans un cadre stable et évolutif pour la suite du projet. Cette maîtrise ne peuvent être obtenue que grâce à un code testé en continu et dont les effets sont reproductibles à volonté. Principes de bases d'un code maintenable ======================================== .. container:: :class:: right *« Il semble que la perfection soit atteinte non quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à retrancher. »* **Antoine de Saint-Exupéry** * Outil de gestion de versions (mercurial, subversion, git, ...) * `PEP 8 -- Style Guide for Python Code`__ * The Zen of Python, by Tim Peters *echo 'import this' >> $PYTHONSTARTUP* __ http://www.python.org/dev/peps/pep-0008/ Outils standards offerts par Python =================================== .. container:: :class:: right *« A clever person solves a problem. A wise person avoids it. »* **Albert Einstein** .. class:: big Les indispensables: - le module logging - le module pydoc - le module unittest - le module pdb Outils standards offerts par Python : logging ============================================= .. class:: big le module logging Le module logging contient des fonctions et des classes construisant un système de journalisation flexible pour vos applications Un logger peut être associé à une sévérité lui indiquant d’ignorer les messages d’importance inférieure à ce niveau .. class:: incremental **Intérêts** - permet de remplacer le "debogage au print" plus efficacement avec la notion de sévérité - multiplicité et arborescence des loggers - met en place une politique de suivi et d'historique des erreurs du programme dès le début du développement et ceci pour toute la vie du projet - configurable par fichier .ini - vous anticipez le travail de maintenance ! .. container:: handout L’envoi d’un message est effectué en appelant une méthode sur une instance de la classe Logger. Ces instances possèdent un nom et sont arrangées selon une hiérarchie en utilisant . comme séparateur Un message est associé à une sévérité (par défaut DEBUG, INFO, WARNING, ERROR ou CRITICAL) Un logger est associé à une ou plusieurs instances de la classe Handler, qui sont responsables de faire parvenir les messages à une destination particulière Les handlers permettent une configuration plus poussées de la journalisation: - utilisation de NTEventLogHandler sous Windows - utilisation de SMTPHandler lors d'erreurs critiques seulement (filtre alors sur la sévérité) - utilisation d'un handler pour l'affichage dans une fenêtre d'application Il est évidemment possible de changer le formattage des messages Vous pouvez mettre au point des règles précise de filtrage avec classe Filter Tout est configurable par fichier .ini ce qui permet à un administrateur de modifier le comportement sans toucher au code Outils standards offerts par Python : logging ============================================= .. container:: small Exemple simple:: import logging # Python >= 2.4 uniquement logging.basicConfig(level=logging.INFO, format=’%(asctime)s %(levelname)s\t\ %(message)s’) logging.debug(’Un message de debogage’) logging.info("De l’information: %s", ’et hop !’) logging.warning(’Attention’) logger = logging.getLogger(’monappli’) logger.debug(’Un message de debogage de mon appli’) logger.info("Plus d’info: %s", ’hop hop hop!’) *donne à l’exécution :* | 2005-11-17 17:24:45,299 INFO De l’information: et hop ! | 2005-11-17 17:24:45,309 WARNING Attention | 2005-11-17 17:24:45,319 INFO Plus d’info: hop hop hop! Pour des exemples plus évolués: http://www.red-dove.com/python_logging.html Outils standards offerts par Python : pydoc =========================================== .. container:: :class:: right *« Programs must be written for people to read, and only incidentally for machines to execute. »* **Abelson and Sussman** .. class:: big le module pydoc Les outils de documentation se basent sur les docstrings qui sont des chaînes de documentation insérées dans le code source de vos programmes python. La fonction builtin help() utilise le module pydoc et permet d’obtenir de l’aide de manière interactive sur un objet vivant pydoc est également un script utilisable dans une console shell à la manière des pages de man sous Unix .. class:: incremental **Intérêts** - pertinence et mise à jour immédiate de la documentation technique - l'information est synthétisée et plus rapide d'accès que de lire le source .. container:: handout Les docstrings se placent sur la première ligne suivant les entêtes de classe ou de fonction ou en tête des fichiers contenant des modules. Ce sont de simples textes. Voir http://www.python.org/dev/peps/pep-0257/ Le module doctest recherche des éléments de textes correspondant à du code en mode intéractif et vérifie son exécution avec le résultat attendu. En utilisant pydoc, vous vous forcer implicitement à écrire des docstring de qualité car vous savez que ce sera relu Démonstration du rendu avec "pydoc pydoc" L'outil epydoc__ est similaire mais permet de documenter plus en détails avec une génération de graphes de dépendances par exemple. __ http://epydoc.sourceforge.net/ Outils standards offerts par Python : unittest ============================================== .. container:: :class:: right *« Let the machine do the dirty work »* **B. W. Kernighan and P. J. Plauger, The Elements of Programming Style** .. class:: big le module unittest Le module unittest fait partie de la bibliothèque standard Il sert à coder des tests unitaires pour des modules - les tests unitaires permettent de tester chaque fonctionnalité offerte par chaque fonction et chaque classe d’un module - dans l’idéal, ils doivent être écrits avant que le module ne soit développé - les tests unitaires d’un module peuvent être nombreux (plusieurs centaines de tests est quelque chose de courant) .. class:: incremental **Intérêts** - garantit une offre continue des fonctionnalités déjà livrées. - les tests unitaires permettent de détecter au plus tôt les erreurs - ces tests sont également de la documentation technique très utile ! Voir http://docs.python.org/lib/module-unittest.html .. container:: handout Automatiser vos tests va grandement améliorer les temps de développements tant en garantissant une offre continue des fonctionnalités déjà livrées. Cela permet de détecter au plus tôt les erreurs N'oubliez pas que le temps allouer aux vérifications et à la validation est souvent largement sous-estimé et atteint en réalité près de 40% Outils standards offerts par Python : unittest (exemple) ======================================================== .. class:: small Code python d'un module calculant une factorielle :: from fact import fact import unittest class FactTC(unittest.TestCase): def test_first_results(self): self.assertEquals(fact(1), 1) for n in xrange(2, 10): self.failUnless(fact(n) == n*fact(n-1)) def test_negative_numbers(self): self.assertRaises(ValueError, fact, -12) if __name__ == ’__main__’: unittest.main() .. container:: handout Pour chaque cas, on crée une classe dérivant de TestCase du module unittest Chaque méthode dont le nom commencera par test sera alors considérée comme un test unitaire (convention) La fonction main() collecte et lance tous les tests définis dans le module, mais il est possible de donner des arguments sur la ligne de commande pour ne lancer que certains cas de tests Un test unitaire est considéré comme passé lorsque l’exécution de la méthode n’a pas généré d’erreur (i.e génération d’une exception) Attention ne pas utiliser le mot clef assert qui est supprimé lors de l’optimisation (python -O) Outils standards offerts par Python : unittest (exemple) ======================================================== .. class:: small Exemple d'erreur retournée :: $ python unittest_compat.py ............F.. ----------------------------------------------- FAIL: test_all (__main__.Py25CompatTC) ----------------------------------------------- Traceback (most recent call last): File "unittest_compat.py", line 183, in test_all self.assertEquals(irange.next(), 2) File "/usr/lib/python2.3/unittest.py", line 302, in failUnlessEqual raise self.failureException, AssertionError: 1 != 2 ----------------------------------------------- Ran 15 tests in 0.120s FAILED (failures=1) .. container:: handout On distinguera deux types d’erreurs lors de l’exécution d’un test : - si une assertion n’est pas vérifiée, une exception AssertionError est lancée, le test échoue, et la docstring du test est utilisée pour le rapport (failure) - si un test génère un autre type d’exception, il s’agit d’une erreur non prévue, le test échoue et la pile d’appels est utilisée pour le rapport (error) Outils standards offerts par Python : pdb ========================================= .. container:: :class:: right *« Tester un programme démontre la présence de bugs, pas leur absence »* **Edsger Dijkstra** .. class:: big le module pdb Python dispose d’un débogueur intégré dans la bibliothèque standard, à travers le module pdb **Quand l'utiliser ?** Un comportement inattendu apparaît et il est impossible d'écrire un test unitaire pour le caractériser Lancer l’interpréteur python avec l’option -i permet de passer en interactif lorsqu’une exception remonte jusqu’à l’interpréteur On peut alors exécuter la fonction pm() du module pdb pour analyser l’exception en détail, de la même façon qu’on analyserait un core dump Plus simple encore, vous pouvez placer ces 2 lignes à n'importe quel endroit de votre code et ceci vous fera rentrer automatiquement dans le débogueur .. class:: small Code python:: import pdb pdb.set_trace() .. container:: handout Lorsqu’un programme se termine avec une exception, la pile d’appels seule n’est pas toujours suffisante pour comprendre ce qu’il s’est passé Il y a deux façons d’utiliser ce débogueur: - Exécuter du code pas à pas - Effectuer une analyse post mortem d’un programme après une exception Outils standards offerts par Python : pdb (exemple) =================================================== .. class:: small Lancement du débogueur à la volée:: | > exemple.py(12)?() | -> A = 2 | (Pdb) l | 7 | 8 A = 1 | 9 | 10 pdb.set_trace() | 11 | 12 -> A = 2 | 13 A = 4 | [EOF] | (Pdb) print A | 1 | (Pdb) next | > exemple.py(13)?() | -> A = 4 | (Pdb) print A | 2 Autres outils possibles (1) =========================== .. class:: big **Environnement pour les tests unitaires** * nose - http://somethingaboutorange.com/mrl/projects/nose/ .. class:: big **Couverture de code par les tests** * trace module - http://docs.python.org/lib/module-trace.html * Coverage.py - http://nedbatchelder.com/code/modules/coverage.html * figleaf - http://darcs.idyll.org/~t/projects/figleaf/doc/ * PyMetrics - http://sourceforge.net/projects/PyMetrics **Problème !** On ne couvre que le code qui est exécuté par les tests unitaires... .. container:: handout Nose est intéressant dans le sens où il peut englober les tests de plusieurs utilitaires différents sous forme de plugin Les outils de couverture de code sont moins efficaces dans le cas de langages dynamiques car seul le code exécuté est testé ! La complexité cyclomatique est un outil de métrologie logiciel développé par Thomas McCabe pour mesurer la complexité d'un programme informatique. Cette mesure comptabilise le nombre de "chemins" au travers d'un programme représenté sous la forme d'un graphe. Autres outils possibles (2) =========================== .. class:: big **Refactoring** * rope - http://rope.sourceforge.net/ * bicyclerepair - http://bicyclerepair.sourceforge.net .. class:: big **Les outils d'analyse de code** * PyChecker - http://pychecker.sourceforge.net/ * pylint - http://www.logilab.org/project/pylint * pyflakes - http://divmod.org/trac/wiki/DivmodPyflakes .. class:: incremental **Problème ?** La nature dynamique du langage python permet plusieurs approches qui provoquent des résultats différents selon les outils. **Lequel utiliser ?** Les trois évidemment ! .. container:: handout La refactorisation du code ne peut intervenir que si un environnement de tests unitaires existe car sinon il est impossible de vérifier que les fonctionnalités sont toujours offertes après la réécriture du code. Refactorisation <==> toujours à fonctionnalités égales C'est une étape délicate qui doit rester exceptionnelle. Pour éviter ce type de changement, il peut être utile de s'aider d'outils d'analyse de code. Ces utilitaires de vérification statique de code pour Python effectuent un travail comparable à celui du vérificateur sémantique d’un compilateur C ou Java. Pyflakes plus rapide: utile pour intégrer dans votre éditeur et vérifier les erreurs de syntaxe et les imports de modules en trop PyChecker utilise l'import de module d'où une certaine exécution du code Pylint est encore trop verbeux par défaut mais il est prévu de corriger ça Fonctionnalités: Conformité PEP 8 Absence de docstring Variables non utilisées Utilisation d’une variable non initialisée Longueur des lignes Module utilisé mais non importé Mauvais nombre d’arguments passés à une méthode ou une fonction Utilisation de l’opérateur % sur des chaînes avec des formats ne correspondant pas aux arguments Utilisation de méthodes ou d’attributs inexistants Redéfinition d’une fonction/classe/méthode dans la même portée self n’est pas le premier argument d’une méthode Argument de méthode/fonction non utilisé (ignore self) Nommage des fonctions, des méthodes, des variables Complexité du code Conclusion ========== | | .. container:: center .. image:: images/billard.jpg .. container:: handout La programmation est une création de l'esprit. Elle n'est pas prévisible et donc vouée par essence au changement. L'assurance qualité vous permet de gérer au mieux ces changements en les **anticipant** et en contrôlant leurs effets. La seule technique de validation et de vérifications sont les tests. Vous devez prévoir le développement de tel sorte qu'il reste testable. Il est donc important d'**isoler** les fonctionnalités et d'éviter les dépendances qui ajoutent des cas d'éxecution indésirables Pour aller plus loin... ======================= `La mailing-liste 'Testing in Python'`__ __ http://lists.idyll.org/listinfo/testing-in-python `PythonTestingToolsTaxonomy wiki page`__ __ http://pycheesecake.org/wiki/PythonTestingToolsTaxonomy `How to misuse code coverage, Brian Marick (1997)`__ __ http://www.testing.com/writings/coverage.pdf Working effectively with legacy code, Michael C. Feathers Refactoring - Improving the design of existing code, Martin Fowler Refactoring to Patterns, Joshua Kerievsky .. container:: incremental :class:: huge center | | Questions ? .. topic:: Links :class: hidden print .. target-notes:: :class: hidden print