Sam & Max » docstring http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Un gros guide bien gras sur les tests unitaires en Python, partie 4 8 http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-4/ http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-4/#comments Sat, 06 Dec 2014 20:34:40 +0000 http://sametmax.com/?p=12717 Après avoir vu pytest, un outil typiquement pythonique sont les doctests, des tests unitaires intégrés dans les docstrings.]]> 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 va 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 :

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 pourrai 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

]]>
http://sametmax.com/un-gros-guide-bien-gras-sur-les-tests-unitaires-en-python-partie-4/feed/ 8
La plus belle manière de parser les arguments de script en python 21 http://sametmax.com/la-plus-belle-maniere-de-parser-les-arguments-de-script-en-python/ http://sametmax.com/la-plus-belle-maniere-de-parser-les-arguments-de-script-en-python/#comments Mon, 03 Mar 2014 14:06:35 +0000 http://sametmax.com/?p=9662 docopt, j'ai largué clize (c'est pas moi chérie, c'est toi) et on vit en amoureux tous les deux en élevant des script dans une ferme dans les Alpes.]]> On peut parser sys.argv à la main, mais c’est fragile.

On peut utiliser getopt ou optpase, mais c’est déprécié.

On peut adopter le nouveau module de la stdlib, vers argparse, mais c’est compliqué.

Alors j’installais toujours clize.

Mais depuis que j’ai rencontré docopt, j’ai largué clize (c’est pas moi chérie, c’est toi) et on vit en amoureux tous les deux en élevant des scripts dans une ferme dans les Alpes.

pip install docopt

Docopt fonctionne à l’envers de la plupart des libs de ce genre : vous écrivez le message d’aide de votre script, et ça génère le parsing des arguments. La syntaxe de l’aide est de type posix, donc si vous avez utilisez un -h ou la commande man un jour, vous la connaissez déjà.

Voici comment ça marche :

help = """Le nom de mon programme trop cool
 
Usage:
  nom_du_script.py <argument_positionel> [<argument_positionel_optionel>] [--flag-optionel]
 
Options:
  -h --help          C'est généré automatiquement.
  --option=<valeur>  Description de l'option.
 
Woot, un footer !
"""
 
from docopt import docopt
 
arguments = docopt(help)
print(arguments)

Si on passe rien de valide au script, l’usage est affiché automatiquement :

$ python nom_du_script.py 
Usage:
  nom_du_script.py <argument_positionel> [<argument_positionel_optionel>] [--flag-optionel]

Si on demande l’aide, en option, ben, l’aide quoi :

$ python nom_du_script.py -h
Le nom de mon programme trop cool
 
Usage:
  nom_du_script.py <argument_positionel> [<argument_positionel_optionel>] [--flag-optionel]
 
Options:
  -h --help          C'est généré automatiquement.
  --option=<valeur>  Description de l'option.
 
Woot, un footer !

Et si on passe des arguments, on les récupère dans un simple dictionnaire :

$ python nom_du_script.py yo
{'--flag-optionel': False,
 '<argument_positionel>': 'yo',
 '<argument_positionel_optionel>': None}
 
$ python nom_du_script.py yo man
{'--flag-optionel': False,
 '<argument_positionel>': 'yo',
 '<argument_positionel_optionel>': 'man'}
 
 $ python nom_du_script.py yo --flag-optionel
{'--flag-optionel': True,
 '<argument_positionel>': 'yo',
 '<argument_positionel_optionel>': None}

Ça peut générer des choses complexes avec des tas de combinaisons d’options, des sous-commandes et tout le bordel.

Généralement on en profite pour faire ça proprement, en mettant l’usage en docstring du script, en calant une version et en rajoutant un if __main__:

 
"""Uber script.
 
Usage:
  schnell.py scheisse 
  schnell.py bier 
 
Options:
  -h --help      _o/
  --version      \o_
  --blitz=krieg  \o/
 
"""
 
from docopt import docopt
 
 
if __name__ == '__main__':
    # __doc__ contient automatiquement la docstring du module
    # en cours
    arguments = docopt(__doc__, version='0.1')
    print(arguments)
$ python schnell.py --version
0.1
 
$ python schnell.py bier
{'bier': True,
 'scheisse': False}
]]>
http://sametmax.com/la-plus-belle-maniere-de-parser-les-arguments-de-script-en-python/feed/ 21
Les docstrings en Python 25 http://sametmax.com/les-docstrings/ http://sametmax.com/les-docstrings/#comments Wed, 06 Mar 2013 09:34:00 +0000 http://sametmax.com/?p=570 Du fait de la nature du tuto, exceptionellement je ne respecterai pas le nouveau format de rédaction. Mais y aura quand même de la zik :

Une des mes fonctionnalités favorites en Python est son mécanisme de documentation du code : les doctrings. En effet, je crois qu’il est très important de rendre simple les tâches over chiantes comme les tests unitaires ou la doc car moins il y a de frein à le faire, plus il y a de chances qu’on le fasse.

Principe

La docstring est une chaîne de caractères que l’on n’assigne pas, et qui est placée à un endroit spécifique du code pour décrire ce dernier.

La docstring la plus courante est placée sous une fonction. Voici une fonction SANS docstring :

def ajouter(a, b):
    return a + b

Et voici une fonction AVEC docstring :

def ajouter(a, b):
    """
        Ajoute deux nombres l'un à l'autre et retourne
        le résultat.
    """
    return a + b

La chaîne de caractère doit être placée juste en dessous de la signature de la fonction.

Écrire des docstrings offrent de nombreux avantages :

  • La fonction help() affiche cette documentation dans un shell.
  • Les outils de programmation tels que les shells ou les IDE affichent cette documentation quand le développeur qui ne lit pas votre code, mais l’utilise, en a besoin.
  • On peut générer une bonne doc du code avec des commandes qui extraient ces docstrings.
  • C’est un mécanisme standardisé de documentation : tout le monde sait que si c’est là, et que ça a cette forme, c’est une documentation.
  • Le code Python peut utiliser la docstring pour la lire ou l’afficher.
  • On peut mettre des tests dans les docstrings, qui servent alors d’exemples d’utilisation.

Usage

Si vous avez une fonction ainsi faite :

def ajouter(a, b):
    """
        Ajoute deux nombres l'un à l'autre et retourne
        le résultat.
    """
    return a + b

Alors dans un shell, toute personne qui va utiliser votre fonction pourra faire :

>>> help(ajouter)
Help on function ajouter in module __main__:
 
ajouter(a, b)
    Ajoute deux nombres l'un à l'autre et retourne
    le résultat.

Il y a ainsi une documentation de votre fonction DANS le shell, sans avoir à se connecter à Internet ou quoi que ce soit. Il n’a pas à ouvrir le moindre fichier.

On peut documenter également les modules en plaçant la docstring comme première expression Python (qui n’est pas un commentaire) tout en haut du fichier :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
 
"""
    Ceci est un module génial qui va faire 
    plein de trucs super cool.
"""
 
import threading
import multiprocessing
from functools import wraps
from Queue import Empty
 
class BaseAsbtractAdapterStrategyFactoryMock(object):
    pass

On peut aussi documenter une classe et ses méthodes :

class ADallas(object):
    """
        Cette classe vous donne le classe à Dallas
        quand vous en avez vraiment besoin.
    """
 
    def univers_impitoyable(self):
        """
            Retourne un objet univers, prêt
            à être impitoyable.
        """

La plupart des fonctions et modules de la lib standard sont ainsi documentées, vous pouvez donc faire :

>>> import os
>>> help(os)
>>> from functools import partial
>>> help(partial)
>>> help(str)
>>> help('foo'.upper)

Bonnes pratiques

D’abord, et malgré mes exemples à caractère purement pédagogique précédents, votre docstring devrait être en anglais. Même quand vous travaillez uniquement avec des français. L’anglais est la lingua franca (oui, oui, je sais…) de l’informatique, et de plus vous évitez tout problème d’encoding car vous n’avez aucun moyen de savoir si cette doc sera lue dans un shell rêglé avec les pieds (comme celui de Windows).

J’écrirai un article pour motiver les résistants à se mettre à l’anglais une bonne fois pour toute.

L’anglais est votre ami. Il est la novlang de notre métier. C’est pas vendeur ça ?

Bref.

Ensuite, il existe plusieurs manières de formater une docstring, et il y a même un PEP 257 qui ne parle que de ça. En résumé :

def foo():
    """Docstring d'une ligne"""
 
 
def foo():
    """Résumé de la docstring de plusieurs lignes.
 
    Contenu détaillé de la doctstring.
    Contenu détaillé de la doctstring.
    Contenu détaillé de la doctstring.
 
    """

Je ne respecte jamais cette convention. Généralement je fais plutôt :

def foo():
    """
        Docstring d'une ligne.
    """
 
def foo():
    """
        Résumé de la docstring de plusieurs lignes.
 
        Contenu détaillé de la doctstring.
        Contenu détaillé de la doctstring.
        Contenu détaillé de la doctstring.
    """

Je trouve ça immensément plus lisible dans le code. Je ne peux pas vous recommander de faire comme moi, puisque c’est aller à l’encontre du PEP. Tout ce que je peux vous dire c’est que personne ne s’est jamais plaint de cette habitude. En matière de docstring, la plupart des gens sont juste déjà trop heureux qu’il y en ait.

En revanche, tout le monde est d’accord sur le fait qu’une ligne de la docstring ne doit pas faire plus de 80 caractères. Donc indentez en conséquence. Le plugin SublimeText Wrap-Plus permet de le faire automatiquement avec Alt + Q (et bien plus). Un must have.

Usage avancé

Python étant un langage qui aime l’instrospection, la docstring est accessible depuis le code sous la forme de l’attribut __doc__ :

>>> def foo():
...     """
...         Can foo a bar with ease
...     """
...     pass
...
>>> foo.__doc__
'\n        Can foo a bar with ease\n    '
>>>

Vous ne vous en servirez pas souvent, mais c’est utile pour créer le help d’un script (c’est ce que fait clize) ou faire une popup dans un IDE.

Une autre particularité des docstrings, c’est qu’elles sont très utilisées dans les générateurs de documentation comme sphinx. Et ils comprennent généralement très bien le format RST.

Le format RST est une convention de balisage pour formater du texte. Il garde le texte lisible, mais permet de générer du HTML, du PDF et un tas d’autres trucs plus propres. Aussi je vous invite à l’utiliser si vous avez une docstring dont vous sentez qu’elle a besoin d’être aussi complète que possible.

Voici toutes les balises à votre disposition :

:param arg1: description
:param arg2: description
:type arg1: type
:type arg1: type
:return: description de la valeur de retour
:rtype: type de la valeur de retour

:Example:

Un exemple écrit après un saut de ligne.

.. seealso:: Référence à une autre partie du code
.. warnings:: Avertissement
.. note:: Note
.. todo:: A faire

On peut aussi utilise ``element`` pour signaler un morceau de code au milieu du texte. Sur les docstrings très longues (il n’est pas rare qu’une docstring soit plus longue que le code qu’elle documente), comme celles des modules, on peut sous-ligner les titres et sous-titres avec des = et des -.

Par exemple :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
"""
    The ``obvious`` module
    ======================
 
    Use it to import very obvious functions.
 
    :Example:
 
    >>> from obvious import add
    >>> add(1, 1)
    2
 
    This is a subtitle
    -------------------
 
    You can say so many things here ! You can say so many things here !
    You can say so many things here ! You can say so many things here !
    You can say so many things here ! You can say so many things here !
    You can say so many things here ! You can say so many things here !
 
    This is another subtitle
    ------------------------
 
    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 
 
"""
 
def add(a, b):
    """
        Adds two numbers and returns the result.
 
        This add two real numbers and return a real result. You will want to
        use this function in any place you would usually use the ``+`` operator
        but requires a functional equivalent.
 
        :param a: The first number to add
        :param b: The second number to add
        :type a: int
        :type b: int
        :return: The result of the addition
        :rtype: int
 
        :Example:
 
        >>> add(1, 1)
        2
        >>> add(2.1, 3.4)  # all int compatible types work
        5.5
 
        .. seealso:: sub(), div(), mul()
        .. warnings:: This is a completly useless function. Use it only in a 
                      tutorial unless you want to look like a fool.
        .. note:: You may want to use a lambda function instead of this.
        .. todo:: Delete this function. Then masturbate with olive oil.
    """
    return a + b

Aucun champ n’est obligatoire, aucuns ne sont interdépendant. Cela vous donne une grande flexibilité pour savoir jusqu’à quel point vous voulez documenter votre fonction.

La section la plus importante à mon sens est :Example:. Avec ça une personne peut généralement avoir une bonne idée de ce qui se passe, et en plus ça sert de tests (comme on le verra plus loin).

La section la plus inutile est de loin .. todo::. En fait je vous recommande de ne pas l’utiliser. Si vous avez des todos, utilisez plutôt la convention de commentaire :

# TODO: un truc à faire

Car :

  • Je pense que vos TODO n’ont rien à foutre dans la doc.
  • Il faut mieux avoir un TODO le plus proche du truc qu’il doit modifier. Le mettre en haut de la fonction n’a pas toujours de sens.
  • De très nombreux outils et services détectent ce format automatiquement et en font quelque chose d’utile.

Le typage des arguments et de la valeur de retour n’est pas toujours utile, surtout avec Python faisant massivement usage du duck typing. La description est plus importante. Mettez le typage quand le type n’est pas intuitif ou signalez une caratéristique comme : itérable, indexable, file-like object, etc.

EDIT: ah, y aussi un field raises pour déclarer que le code peut lever une exception en particulier. J’avais zappé. On m’a aussi demandé si il y avait des équivalent à @since et @depreciated mais non, en général on fout ça dans .. note:: ou .. warning::

Doc tests

Une fonctionalité controversée des docstrings sont les doctests, des tests unitaires directement intégrés dans la docstring.

Mon conseil : utilisez les docstests pour des petites fonctions simples ou pour quelques exemples sur les fonctions complexes, et complétez les avec des tests ordinnaires. Ce sont des bons compléments, mais pas forcément idéales pour contenir TOUS les tests. Après, si c’est le seul truc qui vous motive pour écrire des tests, mettez tout dedans, il vaut mieux ça que rien du tout.

Une doc test est donc la rédaction d’une partie de la docstring avec la syntaxe d’un shell :

def add(a, b):
    """
        Do I neeed to explain this ?
 
        :Example:
 
        >>> add(1, 1)
        2
        >>> add(2.1, 3.4)  # all int compatible types work
        5.5
 
    """
    return a + b
 
# A la fin de votre script, mettez ce snippet qui va activer les doctest
if __name__ == "__main__":
    import doctest
    doctest.testmod()

Si vous importez ce module, il ne se passera rien. Mais si vous faites python script.py, Python va exécuter add(1, 1) et vérifier que cela affiche bien 2, puis exécuter add(2.1, 3.4) et vérifier que cela affiche bien 5.5.

Si il n’y a aucune erreur, le script se termine silencieusement (il donne des détails si on utilise l’option -v). Sinon, il beugle. Par exemple si je rajoute :

>>> add(1, 1)
3

On obtient en sortie :

$ python script.py
*******************************************
File "script.py", line 7, in __main__.add
Failed example:
    add(1, 1)
Expected:
    3
Got:
    2
*******************************************
1 items had failures:
   1 of   2 in __main__.add
***Test Failed*** 1 failures.

Attention !

Python compare non pas la valeur, mais CE QUI S’AFFICHE. Ça peut être très déroutant. Si j’ai les tests :

>>> print str(add(1, 1))        
2        
>>> str(add(1, 1))        
2

Ça va planter :

$ python script.py
*******************************************
File "script.py", line 10, in __main__.add
Failed example:
    str(add(1, 1))
Expected:
    2
Got:
    '2'
*******************************************
1 items had failures:
   1 of   2 in __main__.add
***Test Failed*** 1 failures.

Il aurait fallu que j’écrive :

>>> print str(add(1, 1))
2
>>> str(add(1, 1))
'2'

Notez les guillemets. C’est ainsi que ça s’afficherait dans le shell. Donc c’est ce que teste Python.

C’est la raison pour laquelle les docstests ne sont pas parfaites pour les gros tests. Si vous testez des caratères d’échappements ou du texte unicode, il vous faudra préfixer vos doctests de ur sinon ça va échouer :

ur"""
    Ceci est une doctring écrite en unicode, sans interprétation des caractères
    d'échappement.
"""

Même problème pour les textes longs. Il faut préciser qu’on veut tester une sortie tronquée avec +ELLIPSIS :

>>> print range(1000) # doctest: +ELLIPSIS
[0, 1, ..., 18, 999]

Car vous allez pas écrire les 1000 entiers pour le fun dans le test. Pareil pour les stacktraces.

Les espaces sont signficatifs, du coup il faut parfois marquer les tests avec +NORMALIZE_WHITESPACE :

>>> print range(20) # doctest: +NORMALIZE_WHITESPACE
[0,   1,  2,  3,  4,  5,  6,  7,  8,  9,
10,  11, 12, 13, 14, 15, 16, 17, 18, 19]

Sinon c’est galère car il faut reformater la sortie à la main correctement.

Enfin, sur les structures de données comme les dicos, l’ordre des éléments n’est pas garanti, donc l’ordre d’affichage non plus. Quand aux données aléatoires…

Bref, les docstests, c’est cool, mais il ne faut pas en abuser.

]]>
http://sametmax.com/les-docstrings/feed/ 25
Quelques bonnes raisons de plus d’utiliser iPython 7 http://sametmax.com/quelques-bonnes-raisons-de-plus-dutiliser-ipython/ http://sametmax.com/quelques-bonnes-raisons-de-plus-dutiliser-ipython/#comments Sun, 23 Dec 2012 18:01:36 +0000 http://sametmax.com/?p=3644 iPython, ola, iPython, il déchire sa génitrice avec une poutrelle en verre pilé.]]> Le shell Python est vraiment pratique pour expérimenter, apprendre le langage, tester un snippet vitos ou administrer son site à distance. Mais iPython, ola, iPython, il déchire sa génitrice avec une poutrelle en verre pilé.

Voici quelques commandes qui vous donneront moult raisons de plus pour très vite installer ce shell alternatif.

Autocall

Taper des parenthèses et des guillemets, c’est sooooooo 1995. iPython peut vous les rajouter automatiquement dans les appels de fonctions, il faut juste préfixer de “/”, “,” ou “;” votre appel de fonction pour qu’il fasse la conversion:

/f 1,2 => f(1,2)
,f 1 2 => f("1","2")
;f 1 2 => f("1 2")

Accès au shell système

Préfixez votre commande d’un “!” bien couillu, et iPython va vous exécuter ça dans le shell système (par exemple bash) et vous retourner le résulat:

>>> ! cat /etc/fstab | grep 'ext4'
UUID=e33c5b98-1570-44d6-a32f-5e7970e1e588 /               ext4    errors=remount-ro 0       1

Oui, oui, on est dans un shell Python là. Ou comment bien faire chier la coloration syntaxique de votre blog.

Et on peut mettre tout ça dans une variable et le traiter en Python derrière:

>>> files = !ls
>>> [f.upper() for f in files]
['BUREAU', 'DOCUMENTS', 'EXAMPLES.DESKTOP', 'IMAGES', 'MOD\xc3\xa8LES', 'MUSIQUE', 'PUBLIC', 'T\xc3\xa9L\xc3\xa9CHARGEMENTS', 'UBUNTU ONE', 'VID\xc3\xa9OS',]

A noter que tout est exécuté dans un autre process, ainsi !cd n’aura aucun effet. Mais tout est prévu: %cd et %pwd proxient tout ça vers os.chdir et os.getcwd :-)

Debugger

Entrez %timeit expression pour qu’il qu’iPython vous l’exécute 10000000 fois et vous donne sa performance.

>>> %timeit x=2**100
10000000 loops, best of 3: 22.5 ns per loop

Autre commande: %pdb vous lancera automatiquement votre débuggeur favoris si une exception se lève durant votre session.

Se faciliter la vie

%edit ouvre un editeur (par défault VI sous nunux, mais c’est configurable), et si vous sauvegardez, le contenu tapé est récupéré et exécuté par iPython. Génial pour les bouts de code trop relou à taper sur le prompt.

%gui qt|wx|gtk lance l’intégration de la main loop d’un des toolkits graphiques afin pour de pouvoir faire mumuse avec des widgets sans bloquer votre shell.

Vous étiez vous demandé pourquoi le prompt iPython était plein de In et de Out, et pas de >>> ? Parce que tout l’historique est numéroté, et accessible:

In [38]: 1 + 1
Out[38]: 2
 
In [39]: In[38]
Out[39]: u'1 + 1'
 
In [40]: Out[38]
Out[40]: 2

Mais vous pouvez aussi utiliser %doctest_mode qui donne à votre prompt un air de shell Python normal. L’avantage ? On peut copier le contenu d’un autre shell dedans (et donc d’une doctest) avec les chevrons !

In [49]: %doctest_mode
Exception reporting mode: Plain
Doctest mode is: ON
>>> >>> for pom in ('pom', 'pom', 'pom', 'pom'):
...     ...     print pom
...     ... 
pom
pom
pom
pom
]]>
http://sametmax.com/quelques-bonnes-raisons-de-plus-dutiliser-ipython/feed/ 7