PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Un gros guide bien gras sur les tests unitaires en Python, partie 4   Recently updated !

samedi 6 décembre 2014 à 21:34

Python est un langage très pro, et il y a beaucoup, beaucoup d’outils pour faire des tests.

Après avoir vu pytest, un outil typiquement pythonique sont les doctests, des tests unitaires intégrés dans les docstrings.

Pour rappel, les docstrings, ce sont ces chaînes de caractères qu’on retrouve au début des modules, sous la signature des fonctions ou dans la définition des classes. Elles servent à la documentation de ceux-ci, ainsi on peut la lire dans le code, et dans les vraies docs car les outils standards savent les extraire.

Ça ressemble à ça :

def une_fonction():
    """ Ceci est une docstring.
 
        On peut la lire dans le code source, avec help() dans le shell ou
        dans les docs générés par pydoc et sphinx.
    """
    pass

Et bien ces docstrings, on peut mettre des tests unitaires dedans formatés comme des sessions de shell Python. Cela permet de donner des exemples d’usage, tout en testant son code. C’est chouette.

Musique ?

Musique.

Hello doctests

Faire des doctests n’est pas bien compliqué car c’est du copier coller. On fait une session shell avec ce qu’on veut tester, et on copie-colle le tout dans la docstring. Fastoche.

# on copie juste la session de shell tel quel
def ajouter(a, b):
    """
        >>> ajouter(1, 2)
        3
    """
    return a + b
 
# et on demande à Python de parser les doctests. Directement dans votre fichier
# de code. Si, si. Pas de fichier de tests à part.
if __name__ == "__main__":
    import doctest
    doctest.testmod()

On lance ensuite directement notre fichier de code :

python mon_module.py

Et ça n’affiche absolument rien. C’est parce qu’il n’y a pas d’erreur. On peut avoir le topo en demandant un peu de verbosité avec -v :

python mon_module.py -v
Trying:
    ajouter(1, 2)
Expecting:
    3
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.ajouter
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Les doctests marchent purement en se basant sur le formatage texte. Python prendre la ligne avec >>>, l’exécuter, si la ligne suivante ne contient pas de >>>, il va comparer le résultat de l’exécution de la ligne précédente avec le contenu de la ligne qui la suit.

Ceci passe :

"""
    >>> ajouter(1, 2)
    3
"""

Mais ceci échoue :

"""
    >>> ajouter(1, 2)
    4
"""

Car le résultat AFFICHÉ dans le shell est 3, et non 4.

En cas d’échec, Python vous en dit un peu plus :

python mon_module.py
**********************************************************************
File "mon_module.py", line 6, in __main__.ajouter
Failed example:
    ajouter(1, 2)
Expected:
    4
Got:
    3
**********************************************************************
1 items had failures:
   1 of   1 in __main__.ajouter

Formater ses doctests

Les doctests sont faits pour s’intégrer de manière transparente aux docstrings. On peut donc en mettre autant qu’on veut, au milieu du texte ordinaire de la docstring. Python se base sur les chevrons (>>>) pour savoir quand commence un test, et le saut de ligne pour savoir quand ça se termine. Au delà de ça, le style est libre.

def ajouter(a, b):
    """ Je peux mettre n'importe quoi ici.
 
        Et ici aussi.
 
        Puis intégrer des tests:
 
        >>> ajouter(1, 2)
        3
        >>> ajouter(2, 2)
        4
 
        Et un saut de ligne indique que les tests sont terminés. Mais je peux
        encore en ajouter après si je veux.
 
        >>> ajouter(0, 0)
        0
 
    """
    return a + b

Néanmoins, l’intérêt des doctests est de documenter son code à travers les tests, et donc on adoptera généralement un format tel que ace;:

def ajouter(a, b):
    """ Additionne deux elements.
 
        Exemple :
 
            >>> # on peut mettre des commentaires ici
            >>> ajouter(1, 2) # ou là
            3
            >>> ajouter(2., 2) # fonctionne sur tous les types de nombre
            4.0
 
        La fonction fonctionne en duck typing, et accepte donc tout objet
        qui possède la méthode magique __add__ :
 
            >>> ajouter('a', 'b')
            'ab'
            >>> ajouter([1], [2])
            [1, 2]
    """
    return a + b

Notez comme il est agréable de lire cette docstring : on comprend tout de suite comment utiliser la fonction. En prime, ce sont des tests unitaires qui garantissent que notre fonction va continuer de fonctionner correctement et nous oblige à garder cette doc à jour.

On peut faire des imports dedans ou utiliser temporairement pdb pour debugger. N’importe quel code de shell est valide mais faites attention à ne pas démarrer des boucles infinies comme les event loops des GUI ou lib async.

Voici ce que donnerait l’exemple des articles précédents avec des docstests :

def get(data, index, default=None):
    """ Implémente l'équivalent de dict.get() pour les indexables.
 
        Example :
 
            >>> simple_comme_bonjour = ('pomme', 'banane')
            >>> get(simple_comme_bonjour, 0)
            'pomme'
            >>> get(simple_comme_bonjour, 1000, 'Je laisse la main')
            'Je laisse la main'
    """
    try:
        return data[index]
    except IndexError:
        return default

Problèmes et solutions

Les doctests ne sont pas la Panacée, particulièrement parce que le test se fera sur le résultat AFFICHÉ dans le shell. Cela peut facilement amener à des erreurs.

Déjà, il faut faire attention à la représentation des objets dans le shell Python. La représentation n’est pas forcément la valeur de saisie :

>>> 1.
1.0
>>> "1"
'1'
>>> {"foo": "bar", "une apostrophe : '": "est échapée ainsi qu'un accent"}
{"une apostrophe : '": "est \xc3\xa9chap\xc3\xa9e ainsi qu'un accent", 'foo': 'bar'}

La solution à ce problème est de tester dans le shell les valeurs de retour, et non de le faire de tête. Faites bien gaffe aux espaces qui sont donc significatifs, surtout ceux en fin de ligne. Mon éditeur est configuré pour les virer par défaut, et ça m’a niqué en écrivant l’article :)

Ensuite, il y a des cas où la représentation ne sera pas la même d’un appel à l’autre.

C’est le cas avec les dictionnaires, puisque l’ordre des éléments n’est pas garanti par nature. Ne faites donc pas :

>>> retourne_un_dico()
{'ordre': 'non garanti', 'le': 'resultat'}

Mais plutôt quelque chose qui vous garantit l’affichage :

"""
>>> res = list(retourne_un_dico().items())
>>> res.sort()
[('le', 'resultat'), ('ordre', 'non garanti')]
>>> # ou
>>> retourne_un_dico() == {'ordre': 'non garanti', 'le': 'resultat'}
True
"""

Parfois, on ne peut juste pas garantir l’affichage. Par exemple avec des nombres non prévisibles comme les hash ou les id des objets :

"""
>>> class Test(): pass
>>> repr(Test())
''
"""

7f4687d30fc8 n’est ici pas prévisible. Python met certains cas spéciaux comme celui-ci des flags activables via le commentaire # doctest: +NOM_DU_FLAG.

Par exemple, le flag ELLIPSIS permet de placer ... dans le résultat en guise de joker :

"""
>>> repr(Test()) # doctest: +ELLIPSIS
''
"""

D’autres problèmes similaires peuvent être résolus ainsi. Le flag SKIP permet de sauter un test que vous voulez mettre là, en exemple, mais qui ne doit pas être testé :

"""
>>> # ce test va être ignoré
>>> repr(Test()) # doctest: +SKIP
''
"""

NORMALIZE_WHITESPACE permet de considérer toute séquence de caractères non imprimables comme un espace. 8 tabs ou 4 espaces dans le résultat seront tous considérés comme un espace.

"""
>>> 'ceci est une assez longue ligne divisible' # doctest: +NORMALIZE_WHITESPACE
'ceci    est     une assez longue    ligne divisible'
"""

Les flags sont cumulables, si on les sépare par des virgules dans le commentaire.

Autre astuce, si votre sortie doit contenir un saut de ligne, Python va l’interpréter comme la fin des tests. On peut pallier cela en utilisant <BLANKLINE> :

"""
>>> print('Un saut de ligne\\n')
Un saut de ligne
 
"""

Faites attention aux antislash et autres caractères spéciaux dans vos docstests puisque toute string est parsée deux fois : une fois à l’écriture de la docstring, puis une fois à son exécution. Ici vous voyez que je suis tenu d’échapper mon \n On peut d’ailleurs utiliser les préfixes r (cf: les raw strings) et u sur les docstrings, si un jour vous êtes bloqué par trop d’échappements ou des caractères non ASCII en pagaille, pensez-y.

Un cas particulier est celui des exceptions. LOL, n’est-il pas ?

Pour y répondre, Python décide qu’une expression est levée si il voit Traceback (most recent call last):. Il ignore ensuite tout le texte – qui est donc optionnel et que vous pouvez omettre – jusqu’à ce qu’il rencontre le nom de l’exception levée. À partir de là, il vérifie que le test passe.

Par exemple, si votre exception génère ce traceback :

Traceback (most recent call last):
  File "", line 1, in 
  File "test.py", line 41, in ajouter
    1 / 0
ZeroDivisionError: integer division or modulo by zero

Vous pouvez faire dans votre doctest :

"""
>>> je_leve_une_exception()
Traceback (most recent call last):
ZeroDivisionError: integer division or modulo by zero
"""

Seule la dernière ligne est comparée.

Il est également possible de mettre les doctests dans un fichier texte à part, mais je ne vous le recommande pas. Cela retire l’intérêt principal des doctests : avoir du code exécutable dans la doc. Si on doit avoir un fichier séparé, autant utiliser des tests normaux, bien plus pratiques et complets.

Car il n’y a pas de tear down, setup ou fixtures avec les docstests. Ca reste un outil basique.

Sachez néanmoins que les doctests sont parfaitement compris par pytest, il suffit juste de lui demander de les exécuter avec l’option suivante :

py.test --doctest-modules

Dans ce cas, il n’est pas nécessaire de faire à la fin de chaque fichier contenant des doctests :

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Quand utiliser les doctests ?

Généralement, on utilise un mélange des tests ordinaires (dans notre cas des tests pytest plutôt que unittest) et des doctests.

On utilisera des doctests pour les objets ou les fonctions simples et indépendantes. J’entends par là, des fonctions et des objets qui prennent uniquement des types basiques en paramètres, et qui retournent uniquement ces types basiques en paramètres. Pour les objets, ils doivent avoir peu de méthodes.

Pour tout le reste, on utilisera des tests ordinaires.

Par exemple, si vous avez une fonction comme notre exemple get(), les doctests sont un bon choix. En revanche, si vous avez un objet Server qui est un serveur HTTP, ou une fonction qui prend un objet Server en paramètre, il vaut mieux utiliser les tests ordinaires.

Il est tout à fait possible, et même agréable, de mettre quelques tests simples en doctests qui aident à la documentation, et de faire les tests les plus compliqués via pytest.

Prochaine étape, les mocks. Parti de là, je pourrais vous dire quelles parties de votre programme tester en priorité, et comment. Au début je voulais faire l’inverse, mais finalement, c’est plus pratique.


Télécharger le code de l’article

Dans quels secteurs retrouve-t-on Python ces temps-ci ?   Recently updated !

vendredi 5 décembre 2014 à 21:31

J’utilise le compte Tweeter du blog (n’oubliez pas qu’il y a un flux RSS des tweets) pour RT des offres d’emploi en rapport avec Python régulièrement. Des stages, des demandes de freelance, des embauches, etc. J’essaye de rester dans des offres francophones ou dans des pays limitrophes à la France, histoire que ce soit des infos utiles.

Ce faisant, j’ai pu noter au fil de l’année les secteurs qui sont le plus présents parmi ces recrutements, en tout cas via micro-blogging.

En premier, on a le Web, évidemment. Essentiellement du Django. Rien d’étonnant, ce n’est pas lié à Python en particulier, c’est juste le secteur informatique qui est le plus actif, et tout langage qui peut faire du HTTP correctement amène forcément du taf. Les outils pour cela en Python sont de bonne qualité, et on le retrouve donc un peu partout.

Ensuite vient la finance.

Ceci me gène un peu. Je ne les retweete pas en général, car éthiquement parlant, je ne suis pas à l’aise avec les institutions financières actuelles, mais ce n’est pas le sujet de l’article. Néanmoins ce sont d’excellentes opportunités techniques : analyse de données, temps réel, traitements logiques, UI, grosses infras, des moyens et un travail avec un impact fort IRL. Que Python s’impose dans ces milieux n’est pas une surprise : ils privilégient pragmatisme et efficacité. Or Python est un langage clair, qui permet une itération rapide en dev, des traitements complexes tout en restant facile à maintenir.

En 3ème position (selon les stats de l’INDMS, bien sûr), on retrouve la science : les chercheurs, biologistes, physiciens, géographes, astronomes et tous corps de métiers qui ont besoin de pouvoir adapter facilement des algos, transcrire des maths en code, traiter des gros jeux de données… Python est un langage fantastique pour cela, et sont apparues tout un tas de libs, soit pour gagner en perf, soit pour coller aux règles du métier. On retrouve du coup le langage comme solution de scripting dans des logiciels pros comme les SIG, des bases de données moléculaires et autres gros morceaux. C’est un usage tellement important que la prochaine version va embarquer un nouvel opérateur mathématique pour multiplier les matrices : le @.

Et puis il y a l’admin système et l’automatisation. Python remplace Perl un peu partout, dès qu’un script bash ne suffit plus. Car Python est simplement plus lisible et maintenable que ces deux langages, et il est aussi maintenant plus versatile, grâce au travail de la communauté. Des initiatives comme OpenStack ont fait de Python un acteur majeur dans les solutions de déploiement, de cloud, d’archi distribuées. C’est néanmoins très spécialisé, et concurrentiel, et donc trouver un bon dev là dedans ne doit pas être facile.

Notre pénultième, c’est la gestion d’entreprise. Principalement à travers le produit Odoo (aka OpenERP) qui s’occupe de tout, du personnel aux clients en passant par la facturation. C’est un peu usine à gaz, mais ça vaut toujours mieux que SAP. Ceci dit, en dehors de cela, Python est un fantastique langage glu, du fait qu’il est capable de lire et générer de nombreux formats (MS Office, LibreOffice, PDF, images) et base de données (MySQL, PostGres, Oracle, Access, et d’autres plus obscures). On peut même parler certains protocoles de machines outils. Dans des usines qui ont 15 ans d’empilement technologique et de migrations avec des morceaux legacy qui traînent partout, ça change la vie. Surtout qu’on peut maintenant remplacer VB avec Python presque partout, y compris dans Excel.

En dernier, il y a le Big Data. Derrière ce mot pompeux se cache les analyses de données massives, l’intelligence artificielle et en fait, tout ce qui concerne extraire du sens à partir d’un gros blob qui n’appartient pas aux métiers cités précédemment. Interprétation du langage humain, études d’images ou de son, réseaux sémantiques, généralement sous forme de grandes collections d’échantillons. Là c’est du haut niveau, mais ça fait justement des postes intéressants.

Malgré Kivy, le Rasberry Py et Micro Python, je ne vois pas encore le langage décoller dans l’embarqué, sur téléphone, sur l’internet des objets… WAMP va peut être changer la donne, mais pas tant que qu’on aura pas créé une API plus sexy.

Les jeux vidéos sont aussi un secteur qui manque de Python, et c’est dommage. On lui préfère souvent le Lua, à part dans quelques cas comme Civilisation ou Eve Online.

Apparemment Python paie bien, mais honnêtement j’en ferai pas un critère de choix : ces choses là changent. Si on veut de la thune, on fait du Cobol, emploi bien payé garanti.

Créer un réseau ad hoc sous Windows 8

jeudi 4 décembre 2014 à 16:07

On a pas toujours un switch sous la main, et pourtant, mettre deux ordinateurs en réseau peut se faire sentir. Ok, c’est pour faire une LAN, qui j’essaye de tromper là ?

La solution, c’est le réseau ad hoc, c’est à dire demander à son ordi d’être point d’accès WIFI et réseau local.

Sous Windows 7, c’est facile, panneau de config, réseaux et partages, 3 clics et ça roule.

Mais mon Windows 8 ne voyait pas le réseau, et j’ai steam installé dessus.

Du coup, on a cherché un moyen de faire le réseau depuis W8, et l’option a été retirée des GUI.

Heureusement, il reste la ligne de commande.

D’abord, vérifier que son matos supporte le mode ad hoc :

netsh wlan show drivers

Dans la liste, il doit y avoir une ligne à propos d’un réseau hébergé avec marqué “oui”.

Si ce n’est pas le cas, fin du voyage. Sinon, on configure le réseau (les droits admins sont nécessaires) :

netsh wlan set un_nom_de_reseau mode=allow ssid=un_ssid key=un_mot_de_passe

Quand on veut lancer le réseau :

netsh wlan start un_nom_de_reseau

Quand on veut l’arrêter :

netsh wlan stop un_nom_de_reseau

Et les autres systèmes peuvent le voir dans la liste des réseaux WIFI sous le nom de “un_ssid” avec le mot de passe “un_mot_de_passe”.

Est-ce que tu peux rendre ça configurable ?

mercredi 19 novembre 2014 à 11:55

Il y a cette manie qui traîne, cette idée que si quelque chose n’est pas dans un fichier de configuration, un outil manque de souplesse. Que si il n’y a pas un settings['truc'], ça va être compliqué si on veut le changer plus tard.

Et dans de nombreux langages bas niveaux, ça se vérifie.

Mais quand on code en Python, Ruby, PHP ou Javascript, et d’autant plus avec des gros frameworks d’abstraction, le code est déjà une forme de configuration.

Souvent les clients me demandent de découpler mon code pour le rendre plus souple, plus flexible. Avec des options pour le rendre configurable. Et c’est une bonne chose.

Cependant il faut comprendre qu’il y a déjà tellement de design patterns embarqués dans nos outils modernes (iterator, generator, indexable, sliceable…), tellement de facilités offertes par les paradigmes actuels (typage dynamique, late binding, duck typing, bytecode, etc.) qu’un programme qui n’a pas été écrit comme un porc propose déjà une large marge de manœuvre.

Il est vrai qu’avec l’expérience, on écrit plus vite un code de meilleur qualité, et que, naturellement, on a tendance à laisser des hooks par ci, des opportunités d’injecter des dépendances par là. Surtout si on fait des tests unitaires.

Néanmoins, trop souvent, il sera demandé de pouvoir “rendre cette partie adaptable”, “faire en sorte qu’ici on puisse switcher d’implémentation” ou “je veux pouvoir changer d’avis plus tard”.

On peut déjà le faire.

Je suis programmeur, Python me parle. Un bon dev de la même techno trouvant mon code pourra modifier une bonne partie de mon travail pour obtenir un résultat différent.

Il n’y a pas forcément besoin d’une entrée en plus dans le .ini|json|conf|bak|old|~|/dev/null, d’un paramètre backend, de 2 jours de taf de plus pour rendre adaptable quelque chose qui peut être édité facilement.

De nombreuse fois, il est plus aisé de modifier, casser, remplacer, et même copier / coller.

Ce n’est pas écrit dans les bouquins sur les bonnes pratiques, mais c’est notre métier de coder. On est bons pour ça. On est rapides. Efficaces.

Si la fonctionnalité est un besoin avéré qu’il faudra pouvoir activer et désactiver en prod, bien entendu, il faut une option.

Mais si il faut couvrir un besoin futur hypothétiquement potentiel, laissez le dev décider si c’est YAGNI, on peut gagner beaucoup en productivité en pariant que modifier le code quand le besoin se présentera est la démarche la plus simple.

L’équilibre entre la dette technique et la sur-ingénierie permet de garder un projet sur les rails.

To shave or not to shave ?

mardi 18 novembre 2014 à 22:38

Le sexe fait parti de ces choses qui ne font pas l’unanimité. Pour rien.

Il n’y a qu’à lire les commentaires de nos articles d’opinion (comment tailler une pipe, la sodomie, etc) pour illustrer les avis divergents sur la question.

Mais rien ne m’a semblé plus sujet à désaccord que la question de la pilosité.

Chez la femme, bien entendu, il y a l’épilation aisselles, jambes et maillot. Alors que les touffes sous les bras sont rarement un débat velu, le reste est déjà plus flou.

D’un côté, la majorité des hommes, selon un sondage officiel de l’INDMS, vous répondront qu’ils aiment la sensation de douceur d’un mollet imberbe. D’un autre, combien de gars se sont dit “nan, je vais trop pas coucher avec elle, elle est pas épilée”. J’en ai rencontré deux dans ma vie.

Détail amusant, quand on sait à quel point certaines meufs sont paniquées à l’idée de s’envoyer en l’air le premier soir si elles n’ont pas fait place nette. Surtout qu’un bas nylon coquinement gardé sera souvent fort apprécié, c’est dire si le mâle n’est pas difficile à satisfaire.

Mais le plus houleux, c’est la teuch.

Parce que si le film adulte main stream s’est mis à montrer des abricots chauves partout, ce n’est pas un hasard, c’est le résultat d’un ajustement au marché. Mais dans la VVV, on rencontre de tout. Ceux qui aiment avec, sans, en ticket de métro ou avec juste un pompon pour le clito. Une chance sur cinq de déplaire, selon notre échantillon représentatif de 5 personnes au dîner d’hier.

Et là c’est compliqué. Parce qu’un type qui n’aime pas la forêt tropicale va sans doute sauter la case broutage de pelouse, une étape de jardinage qui a son importance. Quel dommage.

En prime, pour rendre les choses bien plus amusantes, c’est aussi une question de culture. Au japon, les nanas ont l’entre-jambes fleuri, c’est pas de la paresse, c’est la loi de l’offre et la demande. En revanche, certaines femmes musulmanes très voilées ont l’entre-cuisses lisse qui glisse. Délice ou vice ?

Chez l’homme, c’est comme d’hab, moins clair encore.

Déjà vous avez le problème de la calvitie, des poils de tronc et celui de porter la barbe qui se rajoutent. Il y toujours moyen de déplaire à 30% du public, quoi qu’on fasse.

Et comme le modèle marketing du mec torride est sans poil, jusque sur les coucougnettes, on pourrait penser que c’est aussi une représentation empiriquement choisie. Fi, on m’a demandé bien souvent de laisser se garnir mon service 3 pièces car “ça fait acteur porno sinon”. Comprenons donc bien, qu’il n’y aucune garantie d’aller dans le sens du poil.

Mais ne coupons pas les cheveux en quatre, que faire ?

Du pareto, bien sûr !

Je connaissais un gars qui disait, les poils, ça n’a sa place que sur la tête. Sans rentrer dans les extrêmes tirés par les cheveux, les hommes peuvent, plutôt que de se raser ou s’épiler, se tondre. Ça fait propre, mais pas trop. Ça demande de la maintenance, c’est sûr, et aussi de se promener avec une tondeuse, ce qui est bien relou. Il ne faut pas avoir un poil dans la main pour ce genre de chose.

Max a tenté l’épilation définitive des burnes avec une machine à utiliser chez soi. Aux dernières nouvelles il a abandonné en cours de route à son départ pour l’Asie. Je ne puis donc vous en dire plus.

On pourra se consoler en disant que c’est plus chiant encore pour les nanas. Tout arracher est en effet encore le pari le plus sûr. Au pire quelqu’un qui aime jouer sur gazon sera plus tolérant qu’un obsédé du terrain synthétique. Mais ça fait mal. Et ça coûte cher. Enfin les filles, attendez l’accouchement, c’est encore plus douloureux, et ça douille encore plus, sur environ 20 ans. La vie est injuste.

Je ne peux pas terminer cet article sans vous remercier de m’avoir lu.

Poil au cul.

Je ne suis pas désolé.