Blog // Exirel.me

Retrouvez tous les articles liés au tag ProTips via le flux rss dédié à ce tag.

Assert

Par Florian Strzelecki - 00:02 - 13.10.2015

Tags : Python, Bonne pratique, Unit Testing, ProTips, assert

Le mot clé assert, tout le monde connaît à peu près :

def test_your_function()
    assert my_function() == 'good result', (
        'We expect `good result` here.')

Il permet, dans un test unitaire, de vérifier que tout se passe comme prévu. Il est à noter qu'un assert qui se passe bien est un assert qui ne "fait" rien. Il ne fait quelque chose (lever une exception) que lorsque quelque chose ne va pas.

Généralement, il est utilisé dans les tests unitaires, avec pour but de vérifier un aspect particulier du code - d'où souvent le côté "unitaire" de ces tests.

AssertionError

Il est tout à fait possible de recréer l'effet d'un assert avec le bon raise :

>>> assert False, 'We expect True here, not False.'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: We expect True here, not False.

Ceci revient à utiliser raise :

>>> raise AssertionError('We expect True here, not False.')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: We expect True here, not False.

C'est d'ailleurs grâce à cela que le framework unittest peut proposer des méthodes spécifiques pour écrire des tests unitaires. Il se trouve qu'aujourd'hui, d'autres framework de tests en Python reviennent à assert, et préfèrent faire une introspection du code pour proposer une meilleure lecture du résultat du test.

Cela veut dire aussi qu'il est tout à fait possible d'attraper l'exception levée par un assert :

>>> try:
...     assert False, 'Oh no!'
... except AssertionError:
...     print('Error detected.')
... 
Error detected.

Là encore, c'est ce qui permet aux frameworks de tests d'exécuter plusieurs tests à la suite, peu importe qu'ils passent ou qu'ils échouent. assert n'est donc qu'un peu de sucre syntaxique au-dessus d'une fonctionnement somme toute très classique d'exception.

Pas seulement dans les tests.

Partant de là, il est aussi possible d'utiliser assert dans du code en dehors des tests unitaires. Oui, mais comment bien faire ?

Pas comme ça...

Tout d'abords, ne validez jamais les valeurs d'une entrée utilisateur ou d'un système externe avec assert : ce n'est pas son but. Cela veut dire :

Par exemple, si vous devez vérifier que la valeur fournie est bien supérieur à 0, ne faites pas ceci :

assert user_input > 0

Mais cela :

if user_input <= 0:
    raise ValueError(user_input)

assert ne doit jamais servir le fonctionnel.

Il y a une raison simple à cela : l'option -O de python. Voici ce qui se passe lorsque je lance un petit programme sans cette option :

$ python app.py 
Traceback (most recent call last):
  File "app.py", line 7, in <module>
    validate(0)
  File "app.py", line 4, in validate
    assert user_input > 0
AssertionError
$ echo $?
1

Et maintenant avec :

$ python -O app.py
$ echo $?
0

Le assert a été purement et simplement ignoré - ce qui n'est pas du tout le résultat attendu ! Si vous voulez vraiment que votre programme utilise une AssertionError, il vaut mieux la lever vous même plutôt qu'utiliser un assert.

Alors comment ?

Un assert permet de vérifier une condition et de "laisser passer" si elle est vraie : nous pouvons utiliser son pouvoir de ne rien faire si tout se passe bien à notre avantage.

La plupart du temps, une application dispose de deux ou plus niveau de code :

assert est une forme de communication.

Si dans le code de haut niveau nous ne pouvons pas utiliser assert, le code de bas niveau est un excellent candidat pour ça. Par exemple, imaginons une fonction qui divise un nombre par un paramètre donné, mais seulement différent de 0 :

def divide_by(number):
    assert not number == 0, (
        'You can not `divide_by` 0.')
    # process number

Ainsi, un développeur pour s'assurer qu'aucun de ses collègues ne tente d'appeler cette fonction en dehors de son module avec un paramètre incorrect - les développeurs ne lançant que très rarement leur application avec l'option -O.

Personnellement, je l'utilise beaucoup pour valider des keywords argument obligatoire dans du code interne :

def do_something(self, bar=None, **kwargs):
    assert 'foo' in kwargs, 'We expect foo!'
    assert bar is not None, 'We expect a value for bar''
    super().do_something(bar=bar, **kwargs)

De cette façon, je peux faire en sorte de conserver la même signature que la fonction parent, tout en ajoutant des pré-conditions. Cela reste un cas spécifique, et j'invite chacun à faire des choix judicieux et réfléchis.

Digression : couverture de code

Dans un précédent article, je parlais de la couverture de code : assert implique une différence importante pour ladite couverture.

Si vous prenez ces deux bouts de code qui ont l'air identique :

def divide_by(number):
    assert not number == 0, (
        'You can not `divide_by` 0.')
    return CONSTANT / number

def divide_user_input(user_input):
    if user_input == 0:
        raise AssertionError('You can not `divide_by` 0.')
    return CONSTANT / number

Ils peuvent être testés de la même façon :

>>> assert divide_by(1) == CONSTANT
>>> assert divide_user_input(1) == CONSTANT

Cependant, le test de divide_by donnera une couverture de 100%, alors que l'autre fonction n'aura une couverture de seulement 66%.

Pourquoi ? Parce qu'un if génère une branche du code, qu'il faut couvrir aussi, tandis qu'un assert ne génère pas de branche, et n'a pas à être "vérifier" par un test.

C'est une distinction importante, car elle permet de faire la distinction entre :

C'est à vous de juger si une fonction peut être appelée avec des paramètres externes à l'application, ou si cela reste purement en interne. Dans le doute, évitez les assert, mais sinon, c'est un outil très puissant, à la fois pour la simplicité du code, pour éviter des tests "en trop", et, enfin, pour la communication entre développeurs.

Configurer pip en fonction du virtualenv

Par Florian Strzelecki - 23:52 - 17.09.2014

Tags : Python, Bonne pratique, virtualenv, ProTips

Si vous développez avec python, alors vous avez sans doute entendu parler de deux outils très pratiques : virtualenv et pip. Le premier permet des environnements python isolés les uns des autres, et le second permet d'installer des paquets python (que ce soit au niveau global ou dans un environnement isolé).

À ce stade de la lecture, si vous ne savez pas ou ne connaissez pas l'un ou l'autre de ces outils, je ne peux que vous conseiller vivement de vous y intéresser.

Personnellement, depuis que j'ai investis un peu de temps à comprendre et à utiliser ces deux outils, je ne peux plus m'en passer. Cela dit, virtualenv, bien que très pratique, est un peu "brut", et je ne l'utilise plus qu'avec un autre outil, virtualenvwrapper, qui est d'ailleurs nécessaire pour la suite de l'article.

Fichier de configuration pour pip

Pour une raison ou une autre, vous pourriez avoir envie de configurer pip, que ce soit une option particulière (suivre les dépendances par exemple, ou toujours mettre à jour), ou d'ajouter un serveur de distribution. Car oui, même si, par défaut, pip utilise le PYthon Package Index, il peut vous arriver de vouloir utiliser un miroir, voire en cas d'utilisation avancée, votre propre serveur/miroir (ce qui peut arriver assez vite quand vous travaillez dans une équipe qui travaille principalement avec des projets en python).

Pour se faire, vous avez deux options :

Comme ajouter la même option à votre ligne de commande est fastidieux (et qu'une erreur est si vite arrivée), vous aurez plus vite fait d'utiliser un fichier de configuration. Pour cela, vous devez créer le créer au bon endroit : par défaut, il s'agit du fichier $HOME/.pip/pip.conf, et pour notre cas il ressemble à ceci :

[global]
index-url = https://pypi.team.my-companie.biz/dev/+simple/

Oui, .biz, je fais ce que je veux, c'est un exemple et il est tard.

Si vous voulez en savoir un peu plus sur ce fichier de configuration, je vous invite à lire la documentation "PIP User Guide: configuration".

Le soucis maintenant, c'est qu'à chaque fois que vous utiliserez pip, vous passerez forcément par ce serveur, et ce n'est pas forcément ce dont vous avez envie. Personnellement, j'ai plusieurs projets sur mon poste de travail, et tous n'utilisent pas la même configuration.

Configurer pip par virtualenv avec virtualenvwrapper

Si vous n'utilisez pas virtualenv wrapper, je ne peux que vous invitez à considérer ou à reconsidérer son usage : c'est un outil pratique, qui permet une grande flexibilité, et vous évite de nombreuses tâches répétitives au quotidien.

Lorsque vous créez un virtualenv (que ce soit directement avec mkvirtualenv ou via un projet avec mkproject), l'environnement installé dispose d'un ensemble de scripts servant de hook à l'activation et à la désactivation de l'environnement. C'est généralement le bon endroit pour ajouter votre touche personnelle.

Celui qui nous intéresse ici est le script postactivate : en le modifiant, vous pourrez exécuter des commandes et configurer des variables d'environnements.

Il se trouve que l'emplacement du fichier de configuration de pip peut être défini par une variable d'environnement : PIP_CONFIG_FILE. Maintenant, si je vous dis qu'à l'activation de votre environnement, une variable VIRTUAL_ENV indique le répertoire où se trouve les fichiers de l'environnement, vous devriez deviner tout seul ce qu'il vous reste à faire... mais voici un exemple concret de fichier postactivate :

#!/bin/bash
# This hook is sourced after this virtualenv is activated.

export PIP_CONFIG_FILE="$VIRTUAL_ENV/.pip/pip.conf"

Une fois le virtualenv activé, vous pouvez connaître l'emplacement du fichier postactivate avec echo $VIRTUAL_ENV/bin/postactivate.

Et tadam, le tour est joué : vous avez maintenant un fichier de configuration pour pip spécifique à votre environnement. Il ne vous reste plus qu'à y indiquer les options spécifiques, et à ne plus vous inquiéter de savoir quelles sont les options à modifier avant de changer de projet.

Revenir à la configuration globale

Lorsque vous désactivez l'environnement, sans action particulière de votre part, vous conservez la variable PIP_CONFIG_FILE. Ce n'est pas un problème en soit si vous ne travaillez que dans des virtualenvs, mais vous avez sans doute envie de faire les choses proprement.

Pour se faire, c'est le script postdeactivate, placé au même endroit que le script postactivate, qui nous intéresse. Il suffit d'y indiquer la valeur par défaut de PIP_CONFIG_FILE:

#!/bin/bash
# This hook is sourced after this virtualenv is deactivated.

export PIP_CONFIG_FILE="$HOME/.pip/pip.conf"

Et de la même façon, vous trouverez ce script en faisant echo $VIRTUAL_ENV/bin/postdeactivate après avoir activé votre virtualenv.

Il ne vous reste plus qu'à travailler sereinement. Même s'il reste encore un tas d'outils pratiques, là dehors. Et que je n'en connais pas autant que ce que j'aimerais. Et de là à les maîtriser... pfiou...