XVI. Programmation fonctionnelle ▲
XVI-A. Plonger▲
Au Chapitre 13, Tests unitaires, vous avez appris la philosophie des tests unitaires. Au Chapitre 14, Ecriture des tests en premier, vous avez suivi pas à pas l'implémentation de tests unitaires en Python. Au Chapitre 15, Refactorisation, vous avez vu comment les tests unitaires facilitent la refactorisation à grand échelle. Ce chapitre va poursuivre le développement de ces programmes, mais cette fois en mettant l'accent sur des techniques avancées de Python plutôt que sur les test unitaires proprement dit.
Voici un programme Python complet qui remplit le rôle de framework pour les tests de régression. Il prend les tests unitaires que vous avez écrits pour chaque modules, les assemble en une seule suite de tests et les exécute en une seule fois. J'utilise ce script pendant la procédure de construction de ce livre, j'ai des tests unitaires pour plusieurs programmes d'exemple (pas seulement le module roman.py du Chapitre 13, Tests unitaires) et la première chose que mon script de construction automatique fait est d'exécuter ce programme pour être sûr que tous mes exemples fonctionnent encore. Si ce test de régression échoue, la construction s'arrête immédiatement. Je n'ai pas plus envie de publier un exemple qui ne marche pas que vous de le télécharger et de vous gratter la tête face à l'écran en vous demandant ce qui ne va pas.
Exemple 16.1. regression.py▲
Si vous ne l'avez pas déjà fait,vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.
"""Regression testing framework
This module will search for scripts in the same directory named
XYZtest.py. Each such script should be a test suite that tests a
module through PyUnit. (As of Python 2.1, PyUnit is included in
the standard library as "unittest".) This script will aggregate all
found test suites into one big test suite and run them all at once.
"""
import
sys, os, re, unittest
def
regressionTest
(
):
path =
os.path.abspath
(
os.path.dirname
(
sys.argv[0
]))
files =
os.listdir
(
path)
test =
re.compile(
"test\.py$"
, re.IGNORECASE)
files =
filter(
test.search, files)
filenameToModuleName =
lambda
f: os.path.splitext
(
f)[0
]
moduleNames =
map(
filenameToModuleName, files)
modules =
map(
__import__
, moduleNames)
load =
unittest.defaultTestLoader.loadTestsFromModule
return
unittest.TestSuite
(
map(
load, modules))
if
__name__
==
"__main__"
:
unittest.main
(
defaultTest=
"regressionTest"
)
A` l'exécution de ce script dans le même répertoire que le reste des scripts d'exemple de ce livre, l'ensemble des tests unitaires, nommés moduletest.py, sont trouvés, exécutés comme un seul test, réussissant ou échouant ensemble.
Exemple 16.2. Exemple de sortie de regression.py▲
[you@localhost py]$ python regression.py -
v
help should fail with
no object ... ok ***
1
***
help should return
known result for
apihelper ... ok
help should honor collapse argument ... ok
help should honor spacing argument ... ok
buildConnectionString should fail with
list input ... ok ***
2
***
buildConnectionString should fail with
string input ... ok
buildConnectionString should fail with
tuple input ... ok
buildConnectionString handles empty dictionary ... ok
buildConnectionString returns known result with
known input ... ok
fromRoman should only accept uppercase input ... ok ***
3
***
toRoman should always return
uppercase ... ok
fromRoman should fail with
blank string ... ok
fromRoman should fail with
malformed antecedents ... ok
fromRoman should fail with
repeated pairs of numerals ... ok
fromRoman should fail with
too many repeated numerals ... ok
fromRoman should give known result with
known input ... ok
toRoman should give known result with
known input ... ok
fromRoman
(
toRoman
(
n))==
n for
all n ... ok
toRoman should fail with
non-
integer input ... ok
toRoman should fail with
negative input ... ok
toRoman should fail with
large input ... ok
toRoman should fail with
0
input ... ok
kgp a ref test ... ok
kgp b ref test ... ok
kgp c ref test ... ok
kgp d ref test ... ok
kgp e ref test ... ok
kgp f ref test ... ok
kgp g ref test ... ok
----------------------------------------------------------------------
Ran 29
tests in
2.799
s
OK
***1*** Les 5 premiers tests viennent de apihelpertest.py, qui teste le script d'exemple du Chapitre 4, Le pouvoir de l'introspection.
***2*** Les 5 tests suivant viennent de odbchelpertest.py, qui teste le script d'exemple du Chapitre 2, Votre premier programme Python.
***3*** Les tests restant viennent de romantest.py, que vous avez étudié en détail au Chapitre 13, Tests unitaires.
XVI-B. Trouver le chemin▲
Lorsque vous exécutez des scripts Python depuis la ligne de commande, il est parfois utile de savoir l'emplacement sur le disque du script en cours d'exécution.
C'est un de ces trucs obscurs qui sont pratiquement impossibles à deviner seul, mais qu'il est simple de se souvenir une fois que vous l'avez vu. L'élément-clé en est sys.argv. Comme vous l'avez vu au Chapitre 9, Traitement de données XML, c'est une liste qui contient la liste des arguments de ligne de commande. Mais elle contient également le nom du script en cours d'exécution comme il a été appelé depuis la ligne de commande et cela suffit à déterminer son emplacement.
Exemple 16.3. fullpath.py▲
Si vous ne l'avez pas déjà fait,vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.
import
sys, os
print
'sys.argv[0] ='
, sys.argv[0
] ***
1
***
pathname =
os.path.dirname
(
sys.argv[0
]) ***
2
***
print
'path ='
, pathname
print
'full path ='
, os.path.abspath
(
pathname) ***
3
***
***1*** Indépendamment de la manière dont vous exécutez un script, sys.argv[0] contient toujours le nom de ce script, exactement comme il apparaît sur la ligne de commande. Le chemin peut être inclus ou non, comme nous allons le voir.
***2*** os.path.dirname prend un nom de fichier sous forme de chaîne et en retourne la partie qui représente le chemin. Si le nom de fichier ne contient pas d'information de chemin, os.path.dirname retourne une chaîne vide.
***3*** os.path.abspath, l'élément-clé, prend un chemin, qui peut être partiel ou même vide et retourne un chemin complet.
os.path.abspath demande une explication plus détaillée. C'est une fonction très souple, qui peut prendre n'importe quel type de chemin en argument.
Exemple 16.4. Explication détaillée de os.path.abspath▲
>>>
import
os
>>>
os.getcwd
(
) ***
1
***
/
home/
you
>>>
os.path.abspath
(
''
) ***
2
***
/
home/
you
>>>
os.path.abspath
(
'.ssh'
) ***
3
***
/
home/
you/
.ssh
>>>
os.path.abspath
(
'/home/you/.ssh'
) ***
4
***
/
home/
you/
.ssh
>>>
os.path.abspath
(
'.ssh/../foo/'
) ***
5
***
/
home/
you/
foo
***1*** os.getcwd() retourne le répertoire de travail en cours. ***2*** L'appel de os.path.abspath avec une chaîne vide retourne le répertoire de travail en cours, comme os.getcwd(). ***3*** L'appel de os.path.abspath avec un chemin partiel construit un chemin complet à partir de ce chemin partiel, basé sur le répertoire de travail en cours. ***4*** L'appel de os.path.abspath avec un chemin complet le retourne simplement. ***5*** os.path.abspath normalise également le chemin qu'il retourne. Notez que cet exemple marche alors que je n'ai pas de répertoire 'foo', os.path.abspath ne vérifie jamais votre disque, ce n'est que de la manipulation de chaînes.
Les chemins et noms de fichier que vous passez à os.path.abspath n'ont pas besoin d'exister sur le disque.
os.path.abspath ne construit pas seulement des chemins complets, il les normalise. Cela signifie que si vous êtes dans le répertoire /usr/, os.path.abspath('bin/../local/bin') retournera /usr/local/bin. Il normalise le chemin en le rendant aussi simple que possible. Si vous voulez normaliser un chemin de cette manière sans le transformer en chemin complet, utilisez os.path.normpath.
Exemple 16.5. Exemple de sortie de fullpath.py▲
[you@localhost py]$ python /
home/
you/
diveintopython/
common/
py/
fullpath.py ***
1
***
sys.argv[0
] =
/
home/
you/
diveintopython/
common/
py/
fullpath.py
path =
/
home/
you/
diveintopython/
common/
py
full path =
/
home/
you/
diveintopython/
common/
py
[you@localhost diveintopython]$ python common/
py/
fullpath.py ***
2
***
sys.argv[0
] =
common/
py/
fullpath.py
path =
common/
py
full path =
/
home/
you/
diveintopython/
common/
py
[you@localhost diveintopython]$ cd common/
py
[you@localhost py]$ python fullpath.py ***
3
***
sys.argv[0
] =
fullpath.py
path =
full path =
/
home/
you/
diveintopython/
common/
py
***1*** Dans le premier cas, sys.argv[0] inclut le chemin complet du script. Vous pouvez ensuite utiliser la fonction os.path.dirname pour enlever le nom du script et retourner le chemin complet du répertoire, os.path.abspath retournera simplement ce que vous lui donnez.
***2*** Si le script est exécuté avec un chemin partiel, sys.argv[0] contient exactement ce qui a été tapé en ligne de commande. os.path.dirname vous retournera un chemin partiel (relatif au répertoire en cours) et os.path.abspath construira le chemin complet à partir du chemin partiel.
***3*** Si le script est exécuté depuis le répertoire en cours sans donner de chemin, os.path.dirname retournera simplement une chaîne vide. A` partir d'une chaîne vide, os.path.abspath retourne le répertoire en cours, ce qui est bien la valeur recherchée puisque le script a été exécuté depuis le répertoire en cours.
Comme les autres fonctions des modules os et os.path, os.path.abspath est multiplate-forme. Les résultats que vous obtiendrez seront légèrement ifférents si vous utilisez Windows (qui utilise le le backslash comme séparateur de chemin) ou Mac OS (qui utilise les deux points), mais le script fonctionnera. C'est là le rôle du module os.
Exemple 16.6. Exécuter les scripts dans le répertoire en cours▲
import
sys, os, re, unittest
def
regressionTest
(
):
path =
os.getcwd
(
) ***
1
***
sys.path.append
(
path) **
2
***
files =
os.listdir
(
path) ***
3
***
***1*** Au lieu de changer le chemin vers le répertoire ou le script en cours d'exécution est situé, on lui assigne le répertoire en cours. C'est le répertoire dans lequel vous étiez quand vous avez lancé le script, qui n'est pas forcément le même que celui dans lequel se trouve le script (relisez cette phrase jusqu'à que vous ayez compris).
***2*** Ce chemin est ajouté au chemin de recherche des bibliothèques de Python, de manière à ce que Python puisse trouver les modules de tests unitaires lorsque vous les importerez. Vous n'aviez pas besoin de le faire lorsque le chemin était le répertoire du script en cours d'exécution, car Python cherche toujours dans ce répertoire.
***3*** Le reste de la fonction est inchangé.
Cette technique vous permettra de réutiliser ce script regression.py dans d'autres projets. Le script est mis dans un répertoire commun et vous l'exécutez depuis le répertoire du projet. Tous les tests unitaires de ce projet sont trouvés et exécutés, au lieu des tests unitaires du répertoire commun dans lequel est situé regression.py.
XVI-C. Le filtrage de liste revisité▲
Vous êtes déjà familier avec l'utilisation des list comprehensions pour le filtrage de listes. Il y a une autre manière de faire la même chose que certaines personnes trouvent plus expressive.
Python a une fonction filter prédéfinie qui prend deux arguments, une fonction et une liste et retourne une liste.(8) La fonction passée comme premier argument à filter doit elle-même prendre un argument et la liste que filter retournera contiendra tous les éléments de la liste passée en argument pour lesquels la fonction passée à filter retourne vrai.
Vous avez tout compris ? Ce n'est pas aussi difficile que ça en à l'air.
Exemple 16.7. Présentation de filter▲
>>>
def
odd
(
n): ***
1
***
... return
n %
2
...
>>>
li =
[1
, 2
, 3
, 5
, 9
, 10
, 256
, -
3
]
>>>
filter(
odd, li) ***
2
***
[1
, 3
, 5
, 9
, -
3
]
>>>
[e for
e in
li if
odd
(
e)] ***
3
***
>>>
filteredList =
[]
>>>
for
n in
li: ***
4
***
... if
odd
(
n):
... filteredList.append
(
n)
...
>>>
filteredList
[1
, 3
, 5
, 9
, -
3
]
***1*** odd utilise la fonction prédéfinie de modulo «%» pour retourner True si n est impair et False si n est pair. ***2*** filter prend deux arguments, une fonction (odd) et une liste (li). odd est appelé avec chaque élément, au fur et à mesure que la liste est parcourue. Si odd retourne une valeur vraie (rappelez-vous qu'en Python, toute valeur différente de zéro est vraie), l'élément est inclut dans la liste retournée, sinon il est filtré. Le résultat est une liste comprenant uniquement les nombres impairs de la liste originelle, dans le même ordre qu'ils apparaissaient en entrée. ***3*** Vous pouvez accomplir la même chose en utilisant les list comprehensions, comme vous l'avez vu à la Section 4.5, «Filtrage de listes». ***4*** Vous pourriez aussi accomplir la même tâche avec une boucle for. Selon vos habitudes de programmation, cela peut vous sembler plus «logique», mais les fonctions comme filter sont bien plus expressives. Non seulement elles sont plus simples à écrire, elles sont aussi plus simple à lire. Lire la boucle for est comme regarder un tableau de trop près, on voit tous les détails, mais il faut quelques secondes pour comprendre l'ensemble : «Oh, c'est juste un filtrage de liste !»
Exemple 16.8. filter dans regression.py▲
files =
os.listdir
(
path) ***
1
***
test =
re.compile(
"test\.py$"
, re.IGNORECASE) ***
2
***
files =
filter(
test.search, files) ***
3
***
***1*** Comme vous l'avez vu à la Section 16.2, «Trouver le chemin», path peut contenir le chemin complet ou partiel du répertoire du script en cours d'exécution, ou une chaîne vide si le script est exécuté depuis le répertoire en cours. De toute manière, files contiendra les noms des fichiers qui sont dans le même répertoire que le script en cours d'exécution.
***2*** Ceci est une expression régulière compilée. Comme vous l'avez vu à la Section 15.3, «Refactorisation», si vous devez utiliser la même expression régulière de manière répétée, il vaut mieux la compiler pour obtenir de meilleures performances. L'objet compilé a une méthode search qui prend un seul argument, la chaîne à rechercher. Si l'expression régulière reconnaît la chaîne, la méthode search retourne un objet Match contenant des informations sur ce qu'elle a trouvé, sinon elle retourne None, la valeur nulle de Python.
***3*** Pour chaque élément de la liste files, la méthode search de l'objet expression régulière compilée test va être appelée. Si l'expression régulière reconnaît la chaîne, la méthode retourne un objet Match, que Python considère comme ayant pour valeur vrai, donc l'élément sera inclus dans la liste retournée par filter. Si l'expression régulière ne reconnaît pas la chaîne, la méthode search retourne None, ce que Python considère comme ayant pour valeur faux et donc l'élément ne sera pas inclus.
Note historique. Les versions de Python antérieures à la version 2.0 n'avaient pas de list comprehensions, donc on ne pouvait pas filtrer avec des list comprehensions; la fonction filter était la seule possibilité. Malgré l'introduction des list comprehensions dans la version 2.0, certaines personnes préfèrent encore l'ancien style avec filter (et sa fonction associée, map, que vous verrez un peu plus loin dans ce chapitre). Les deux techniques fonctionnent actuellement, le de l'une ou l'autre est donc une question de style. Il est question de ne plus supporter map et filter dans les futures versions de Python, mais aucune décision n'a été prise pour le moment.
Exemple 16.9. Filtrage avec des list comprehensions▲
files =
os.listdir
(
path)
test =
re.compile(
"test\.py$"
, re.IGNORECASE)
files =
[f for
f in
files if
test.search
(
f)] ***
1
***
***1*** On obtient exactement le même résultat qu'avec la fonction filter. Quelle est la manière la plus expressive ? A` vous de le dire.
XVI-D. La mutation de liste revisitée▲
Vous avez déjà vu comment appliquer les list comprehensions aux mutations de listes. Il y a une autre manière d'obtenir la même chose en utilisant la fonction prédéfinie map. Elle fonctionne de manière similaire à la fonction filter.
Exemple 16.10. Présentation de map▲
>>>
def
double
(
n):
... return
n*
2
...
>>>
li =
[1
, 2
, 3
, 5
, 9
, 10
, 256
, -
3
]
>>>
map(
double, li) ***
1
***
[2
, 4
, 6
, 10
, 18
, 20
, 512
, -
6
]
>>>
[double
(
n) for
n in
li] ***
2
***
[2
, 4
, 6
, 10
, 18
, 20
, 512
, -
6
]
>>>
newlist =
[]
>>>
for
n in
li: ***
3
***
... newlist.append
(
double
(
n))
...
>>>
newlist
[2
, 4
, 6
, 10
, 18
, 20
, 512
, -
6
]
***1*** map prend une fonction et une liste(9) et retourne une nouvelle liste en appelant la fonction avec chaque élément de la liste dans l'ordre. Dans ce cas, la fonction multiplie simplement chaque élément par 2. ***2*** Vous pouvez accomplir la même tâche avec une list comprehension. Les list comprehensions ont été introduite pour la première fois dans Python 2.0, map en a toujours fait partie. ***3*** Vous pouvez, si vous insistez pour penser comme un programmeur Visual Basic, utiliser une boucle for pour accomplir la même tâche.
Exemple 16.11. map avec des listes de types mélangés ▲
>>>
li =
[5
, 'a'
, (
2
, 'b'
)]
>>>
map(
double, li) ***
1
***
[10
, 'aa'
, (
2
, 'b'
, 2
, 'b'
)]
***1*** Je précise que map fonctionne tout à fait avec des listes de types mélangés, tant que la fonction que vous utilisez est capable de traiter chaque type. Dans ce cas, la fonction double multiplie simplement l'argument qui lui est passé par 2 et Python A Le Bon Réflexe et agit en fonction du type de l'argument. Pour les entiers, cela veut dire les multiplier effectivement par 2, pour les chaînes, les concaténer avec elles-mêmes, pour les tuples, en créer un nouveau contenant deux fois la série d'éléments du précédent.
Mais passons maintenant à du véritable code.
Exemple 16.12. map dans regression.py▲
filenameToModuleName =
lambda
f: os.path.splitext
(
f)[0
] ***
1
***
moduleNames =
map(
filenameToModuleName, files) ***
2
***
***1*** Comme vous l'avez vu dans la Section 4.7, «Utiliser des fonctions lambda», lambda définit une fonction incluse. Et comme vous l'avez vu dans l'Exemple 6.17, «Division de noms de chemins», os.path.splitext prend un nom de fichier et retourne un tuple (nom, extension). Donc filenameToModuleName est une fonction qui prend un nom de fichier et en supprime l'extension, ne retournant que le nom.
***2*** L'appel de map prend chaque nom de fichier de files, le passe à la fonction filenameToModuleName et retourne une liste des valeurs de retour de chacun de ces appels de fonction. Autrement dit, on enlève l'extension de chaque nom de fichier et on stocke la liste de ces noms sans extension dans moduleNames.
Comme vous le verrez dans le reste de ce chapitre, vous pouvez étendre ce type d'approche centrée sur les données jusqu'au but final, qui est de définir et d'exécuter une suite de teste unique contenant les tests de toutes les suites individuelles.
XVI-E. Programmation centrée sur les données▲
Maintenant, vous vous demandez certainement pourquoi tout ça est mieux que d'utiliser des boucle for et de simples appels de fonction. C'est une question tout à fait justifiée. En fait, c'est avant tout une question de perspective, utiliser map et filter vous oblige à centrer votre réflexion sur les données.
Dans le cas présent, nous avons commencé absolument sans données, la première chose que nous avons faite est d'obtenir le chemin du répertoire du script en cours, puis une liste des fichiers de ce répertoire. Cette amorce nous a amené de véritables données avec lesquelles travailler : une liste de noms de fichiers.
Cependant, nous ne nous intéressons pas à tous ces fichiers, seulement à ceux qui sont des suites de tests. Nous avions trop de données, nous avions donc besoin de les filtrer. Comment savoir quelles données conserver ? Nous avions besoins d'un test pour le décider, nous en avons donc créé un que nous avons passé à la fonction filter. Dans le cas présent, nous avons utilisé une expression régulière, mais le concept serait le même quelle que soit la manière dont le test serait constitué.
Nous avions donc les noms de fichiers de chaque suite de tests (et seulement des suites de tests puisque le reste a été filtré), mais ce que nous voulions précisément c'était des noms de modules. Nous avions la quantité de données correcte, mais elles étaient dans un mauvais format. Nous avons donc défini une fonction qui transformerait un nom de fichier en nom de module et l'avons appliquée à toute la liste. D'un nom de fichier, nous obtenons un nom de module, d'une liste de noms de fichiers, une liste de noms de modules.
A` la place de filter, nous aurions pu utiliser une boucle for avec une instruction if. Au lieu de map, nous aurions plus utiliser une boucle for avec un appel de fonction. Mais utiliser des boucles de cette manière est un travail de tâcheron. Au mieux, c'est une perte de temps, au pire, on introduit des bogues difficile à détecter. Par exemple, il faut de toute manière définir la condition de test pour «ce fichier est-il une suite de test ?», c'est la logique spécifique de l'application et aucun langage ne va l'écrire à notre place. Mais une fois que l'on a résolu cette question, pourquoi s'épuiser à définir une nouvelle liste vide, écrire une boucle for, une instruction if et appeller manuellement append pour ajouter chaque élément à la nouvelle liste si il répond à la condition et ensuite garder trace de quelle variable contient la nouvelle liste et de celle qui contient l'ancienne ? Pourquoi ne pas nous contenter de définir la condition de test et laisser Python faire le reste du travail pour nous ?
Oh, bien sûr, vous pouvez montrer plus d'astuce et supprimer les éléments en place sans créer de nouvelle liste. Mais cela vous a déjà joué des tours. Essayer de modifier une structure de données que vous êtes en train de parcourir peut être périlleux. Vous supprimez un élément, bouclez sur le suivant et voilà que vous en avez sauté un. Est-ce que Python fait partie des langages qui fonctionnent de cette manière ? Combien de temps vous faudrait-il pour le découvrir ? Et est-ce que vous vous rappellerez si c'est sûr la prochaine fois que vous essayerez ? Les programmeurs passent tellement de temps et avec tellement d'erreurs, à se préoccuper de problème purement techniques de cet ordre, tout cela est inutile. Cela n'avance en rien votre programme, ce n'est que du travail de tâcheron.
J'étais réticent aux list comprehensions quand j'ai appris Python et j'ai résisté à filter et map encore plus longtemps. Je tenais à me rendre la vie plus difficile en me cantonnant à la manière familière des boucles for et des instructions if, à la programmation pas à pas, centrée sur le code. Et mes programmes Python ressemblaient beaucoup à des programmes Visual Basic, détaillant chaque étape de chaque opération dans chaque fonction. Et ils avaient tous les mêmes petits problèmes et les mêmes bogues difficiles à détecter. Et tout cela était inutile.
Laissons tout cela derrière nous. Le code de tâcheron n'est pas important. Les données sont importantes et les données ne sont pas compliquées. Ce ne sont que des données, s'il y en a trop, filtrez-les, si elles ne sont pas au bon format, transformez-les. Concentrez-vous sur les données, abandonnez le travail de tâcheron.
XVI-F. Importation dynamique de modules▲
Assez de discours philosophiques. Parlons plutôt de l'importation dynamique de modules.
D'abord, regardons à l'importation normale de modules. La syntaxe import module regarde dans le chemin de recherche si il y a un module portant ce nom et l'importe. Vous pouvez également importer plusieurs modules en une seule fois de cette manière, en les séparant par des virgules. Nous l'avons fait à la toute première ligne du script de ce chapitre.
Exemple 16.13. Importation de plusieurs modules à la fois▲
import
sys, os, re, unittest ***
1
***
***1*** Cette instruction importe quatre modules à la fois : sys (pour les fonctions systèmes et l'accès aux paramètres de ligne de commande), os (pour les fonctions liées au système d'exploitation comme obtenir la liste de fichiers du répertoire), re (pour les expressions régulières) et unittest (pour les tests unitaires).
Maintenant, nous allons faire la même chose mais par importation dynamique.
Exemple 16.14. Importation dynamique de modules▲
>>>
sys =
__import__
(
'sys'
) ***
1
***
>>>
os =
__import__
(
'os'
)
>>>
re =
__import__
(
're'
)
>>>
unittest =
__import__
(
'unittest'
)
>>>
sys ***
2
***
>>>
<
module 'sys'
(
built-
in
)>
>>>
os
>>>
<
module 'os'
from
'/usr/local/lib/python2.2/os.pyc'
>
***1*** La fonction prédéfinie __import__ remplit la même tâche que l'utilisation de l'instruction import, mais c'est une fonction qui prend une chaîne en argument.
***2*** La variable sys est maintenant le module sys, comme si nous avions juste écrit import sys. La variable os est maintenant le module os etc.
Donc __import__ importe un module, mais prend une chaîne en argument. Dans ce cas, le module que nous importons est une chaîne littérale, mais il pourrait tout au si bien s'agir d'une variable ou du résultat d'un appel de fonction. Et le nom de la variable à laquelle vous assignez le module n'a pas besoin de correspondre au nom du module. Nous pourrions importer une série de modules et les assigner à une liste.
Exemple 16.15. Importation d'une liste de modules▲
>>>
moduleNames =
['sys'
, 'os'
, 're'
, 'unittest'
] ***
1
***
>>>
moduleNames
['sys'
, 'os'
, 're'
, 'unittest'
]
>>>
modules =
map(
__import__
, moduleNames) ***
2
***
>>>
modules ***
3
***
[<
module 'sys'
(
built-
in
)>
,
<
module 'os'
from
'c:\Python22\lib\os.pyc'
>
,
<
module 're'
from
'c:\Python22\lib
\r
e.pyc'
>
,
<
module 'unittest'
from
'c:\Python22\lib\unittest.pyc'
>
]
>>>
modules[0
].version ***
4
***
'2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)]'
>>>
import
sys
>>>
sys.version
'2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)]'
***1*** moduleNames est juste une liste de chaînes. Rien de particulier si ce n'est que les chaînes ressemblent à des noms de modules que nous pourrions importer.
***2*** Et voilà, nous voulions les importer et nous l'avons fait en appliquant la fonction __import__ à la liste, ce qui prend chaque élément de la liste (moduleNames), le passe en argument à la fonction (__import__) et construit une nouvelle liste avec les valeurs retournées par cette fonction.
***3*** ous avons donc construit une liste de modules à partir d'une liste de chaînes (Vos chemins peuvent être différents en fonction de votre système d'exploitation, de l'emplacement de votre installation Python etc.)
***4*** Pour bien retenir qu'il s'agit de véritables modules, regardons quelques uns de leurs attributs. Rappelez-vous que modules[0] est le module sys, donc modules[0].version est sys.version. Tous les autres attributs et méthodes de ces modules sonts également disponibles. Il n'y a rien de magique à propos de l'instruction import, ni à propos des modules. Les modules sont des objets. Tout est objet.
Maintenant vous devez être en mesure d'assembler tout cela et de comprendre ce que fait la majeure partie du code d'exemple de ce chapitre.
XVI-G. Assembler les pièces▲
Vous en avez assez appris pour déconstruire les sept premières lignes du code d'exemple de ce chapitre : lire un répertoire et importer des modules sélectionnés parmi ceux qu'il contient.
Exemple 16.16. La fonction regressionTest▲
def
regressionTest
(
):
path =
os.path.abspath
(
os.path.dirname
(
sys.argv[0
]))
files =
os.listdir
(
path)
test =
re.compile(
"test\.py$"
, re.IGNORECASE)
files =
filter(
test.search, files)
filenameToModuleName =
lambda
f: os.path.splitext
(
f)[0
]
moduleNames =
map(
filenameToModuleName, files)
modules =
map(
__import__
, moduleNames)
load =
unittest.defaultTestLoader.loadTestsFromModule
return
unittest.TestSuite
(
map(
load, modules))
Regardons cela ligne par ligne. Supposons que le répertoire en cours est c:\diveintopython\py, qui contient les exemples du livre, y compris le script de ce chapitre. Comme vous l'avez vu à la Section 16.2, «Trouver le chemin», le répertoire du script est assigné à la variable path, commençons donc à cette étape.
Exemple 16.17. Etape 1 : Obtenir la liste des fichiers▲
>>>
import
sys, os, re, unittest
>>>
path =
r'c:\diveintopython\py'
>>>
files =
os.listdir
(
path)
>>>
files ***
1
***
['BaseHTMLProcessor.py'
, 'LICENSE.txt'
, 'apihelper.py'
, 'apihelpertest.py'
,
'argecho.py'
, 'autosize.py'
, 'builddialectexamples.py'
, 'dialect.py'
,
'fileinfo.py'
, 'fullpath.py'
, 'kgptest.py'
, 'makerealworddoc.py'
,
'odbchelper.py'
, 'odbchelpertest.py'
, 'parsephone.py'
, 'piglatin.py'
,
'plural.py'
, 'pluraltest.py'
, 'pyfontify.py'
, 'regression.py'
, 'roman.py'
, 'romantest.py'
,
'uncurly.py'
, 'unicode2koi8r.py'
, 'urllister.py'
, 'kgp'
, 'plural'
, 'roman'
,
'colorize.py'
]
***1*** files est une liste de tous les fichiers et les répertoires du répertoire du script (si vous avez déjà exécuté certains exemples, vous verrez également des fichiers .pyc).
Exemple 16.18. Etape 2 : Filtrage des fichiers▲
>>>
test =
re.compile(
"test\.py$"
, re.IGNORECASE) ***
1
***
>>>
files =
filter(
test.search, files) ***
2
***
>>>
files ***
3
***
['apihelpertest.py'
, 'kgptest.py'
, 'odbchelpertest.py'
, 'pluraltest.py'
, 'romantest.py'
]
***1*** Cette expression régulière reconnaît toutes les chaînes qui finissent par test.py. Notez que nous devons utiliser le caractère d'échappement pour le point, un point dans une expression régulière signifiant «n'importe quel caractère», ce que nous voulons c'est bien un point.
***2*** L'expression régulière compilée agit comme une fonction, nous pouvons donc l'utiliser pour filtrer la liste de fichiers et de répertoires.
***3*** Ce qu'il reste est la liste des scripts de tests unitaires puisque ce sont les seuls nommés QUELQUECHOSEtest.py.
Exemple 16.19. Etape 3 : Mutation des noms de fichiers en noms de modules ▲
>>>
filenameToModuleName =
lambda
f: os.path.splitext
(
f)[0
] ***
1
***
>>>
filenameToModuleName
(
'romantest.py'
) ***
2
***
'romantest'
>>>
filenameToModuleName
(
'odchelpertest.py'
)
'odbchelpertest'
>>>
moduleNames =
map(
filenameToModuleName, files) ***
3
***
>>>
moduleNames ***
4
***
['apihelpertest'
, 'kgptest'
, 'odbchelpertest'
, 'pluraltest'
, 'romantest'
]
***1*** Comme vous l'avez vu à la Section 4.7, «Utiliser des fonctions lambda», lambda est une manière rapide de créer des fonctions incluses d'une ligne. Celle-ci prend un nom de fichier avec une extension et le retourne sans son extension en utilisant la fonction de la bibliothèque standard os.path.splitext que vous avez vu à l'Exemple 6.17, «Division de noms de chemins».
***2*** filenameToModuleName est une fonction. Il n'y a rien qui différencie les fonctions lambda des fonctions habituelles définies par l'instruction def. Nous pouvons appeler la fonction filenameToModuleName comme n'importe quelle autre et elle fait exactement ce que nous voulons qu'elle fasse : enlever l'extension du nom de fichier passé en argument.
***3*** Maintenant nous pouvons appliquer cette fonction à chaque nom de fichier de la liste de fichier de tests unitaires à l'aide de map.
***4*** Le résultat est bien ce que nous souhaitons : une liste de modules sous forme de chaînes.
Exemple 16.20. Etape 4 : Mutation des noms de modules en modules▲
>>>
modules =
map(
__import__
, moduleNames) ***
1
***
>>>
modules ***
2
***
[<
module 'apihelpertest'
from
'apihelpertest.py'
>
,
<
module 'kgptest'
from
'kgptest.py'
>
,
<
module 'odbchelpertest'
from
'odbchelpertest.py'
>
,
<
module 'pluraltest'
from
'pluraltest.py'
>
,
<
module 'romantest'
from
'romantest.py'
>
]
>>>
modules[-
1
] ***
3
***
<
module 'romantest'
from
'romantest.py'
>
***1*** Comme vous l'avez vu à la Section 16.6, «Importation dynamique de modules», nous pouvons utiliser map et __import__ pour transformer une liste de noms de modules (sous forme de chaînes) en une liste de modules (que nous pouvons appeler comme n'importe quel autre module).
***2*** modules est maintenant une liste de modules, totalement accessibles comme tout autre module.
***3*** Le dernier module de la liste est le module romantest, comme si nous avions écrit import romantest.
Exemple 16.21. Etape 5 : Chargement des modules en une suite de tests▲
>>>
load =
unittest.defaultTestLoader.loadTestsFromModule
>>>
map(
load, modules) ***
1
***
[<
unittest.TestSuite tests=
[
<
unittest.TestSuite tests=
[<
apihelpertest.BadInput testMethod=
testNoObject >
] >
,
<
unittest.TestSuite tests=
[<
apihelpertest.KnownValues testMethod=
testApiHelper>
] >
,
<
unittest.TestSuite tests=
[
<
apihelpertest.ParamChecks testMethod=
testCollapse>
,
<
apihelpertest.ParamChecks testMethod=
testSpacing>
] >
,
...
]
]
>>>
unittest.TestSuite
(
map(
load, modules)) ***
2
***
***1*** Ce sont de véritable objets-modules. Nous pouvons non seulement y accéder comme à tout autre module, instancier des classes et appeler des fonctions, nous pouvons également utiliser l'instrospection pour déterminer quelles fonctions et classes il contient. C'est ce que la méthode loadTestsFromModule fait : elle utilise l'instrospection et retourne un objet unittest.TestSuite pour chaque module. Chaque objet TestSuite contient en fait une liste d'objets TestSuite, un pour chaque classe TestCase du module et chacun de ces objets TestSuite contient une liste de tests, un pour chaque méthode de test du module.
***2*** Finalement, nous regroupons la liste d'objets TestSuite en une seule suite de tests. Le module unittest n'a aucun mal à parcourir cet arbre de suites de tests imbriquées, il recherche une méthode de test, l'exécute, vérifie que le test passe ou échoue et continue de parcourir l'arbre jusqu'à la prochaine méthode de test.
Ce processus d'introspection est ce que le module unittest fait d'habitude pour nous. Vous vous rappelez de cette fonction magique unittest.main() que nos modules de test appelaient pour démarrer le processus ? unittest.main() crée en fait une instance de unittest.TestProgram, qui crée à son tour une instance de unittest.defaultTestLoader et le charge avec le module appelant (comment obtient-il une référence au module appelant sans qu'on lui en donne une ? En utilisant une instruction tout aussi magique, __import__('__main__'), qui importe dynamiquement le module en cours d'exécution. Je pourrais écrire un livre sur tous les trucs et les techniques utilisé dans le module unittest, mais dans ce cas je ne finirais jamais celui-ci).
Exemple 16.22. Etape 6 : Passage de la suite de tests à unittest▲
if
__name__
==
"__main__"
:
unittest.main
(
defaultTest=
"regressionTest"
) ***
1
**
***1*** Au lieu de laisser le module unittest opérer sa magie pour nous, nous avons fait la majeure partie du travail nous-même. Nous avons créé une fonction (regressionTest) qui importe les modules, appelé unittest.defaultTestLoader et regroupé l'ensemble en une suite de tests. Maintenant, tout ce dont nous avons besoins est de dire à unittest qu'il doit, au lieu de rechercher des tests et de construire une suite de tests de la manière habituelle, appeler simplement la fonction regressionTest, qui retourne une TestSuite prête à l'emploi.
XVI-H. Résumé▲
Le programme regression.py et sa sortie doivent maintenant être parfaitement clairs.
Vous devez maintenant être à l'aise avec les notions suivantes :
- Manipuler les information de chemin depuis la ligne de commande.
- Filtrer des listes à l'aide de filter au lieu des list comprehensions.
- Faire des mutations de listes à l'aide de map au lieu des list comprehensions.
- Importer des modules dynamiquement.