Billets plus anciens »

Python et les tests (nose1, nose2, python -m unittest)

Quand j'ai commencé à développer en Python, j'ai très rapidement utilisé Nose pour exécuter mes tests unitaires.

Je souhaite dans ce billet faire un petit point sur les différentes versions de Nose (Nose version 1, Nose version 2) ainsi que la commande discover de unittest2.

Nose1, Nose2 et python -m unittest discover ?

Ces trois outils ont pour fonction de trouver (discover) les tests d'un projet, de les exécuter et d'afficher le bilan de l'exécution des tests.

Les auteurs de ces outils

Nose1 et Nose2 sont principalement développés par Jason Pellerin.

Unittest est principalement développé par Michael Foord.

Utilisation de Nose

Je vais commencer par un très court exemple d'utilisation de Nose.

Ce que j'ai tout de suite aimé avec Nose, c'est que l'on peut commencer par écrire des tests très simples. Exemple, d'un fichier nommé test1.py :

def test_foo():
    a = 1
    assert a == 1

def test_bar():
    b = 'data'
    assert b == 'data'

Lancement de la commande nose :

$ bin/nosetests -v
test1.test_foo ... ok
test1.test_bar ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

Dans cet exemple, tout est minimaliste, pas de classes du type TestCase, de TestSuite

Pour débuter un projet, cette simplicité est bien pratique même si par la suite il est bon de migrer peu à peu vers une syntax Unittest (toujours exécutable par nose) afin de tirer profit par exemple des fonctionnalités suivantes :

Exemple :

import unittest

class MyTest(unittest.TestCase):
    def setUp(self):
        self.session = 'foobar'

    def tearDown(self):
        self.session = None

    def test_foo(self):
        a = 1
        self.assertEqual(a, 1)

    def test_bar(self):
        b = 'data' + self.session
        self.assertEqual(b, 'datafoobar')

L'exécution de ce test avec nose2 :

$ bin/nose2 -s tests/ -v
test_bar (test_basic.MyTest) ... ok
test_foo (test_basic.MyTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

En 2013, est-ce que je dois utiliser Nose1 ou Nose2 ?

Tout d'abord, un peu d'histoire :

Pour simplifier, un des objectifs de Nose2 est de tirer parti des capacités d'extensions de unittest2 (je ne peux pas en dire plus, je ne connais pas le code interne de Nose).
D'après l'auteur, le code de Nose2 est plus petit et plus simple à maintenir. Illustration :
  • le coeur de Nose1 fait environ 3000 lignes de code
  • le coeur de Nose2 fait environ 1300 lignes de code

Une page de la documentation de nose2 qui liste les différences entre nose, nose2 et unittest.

Début 2013, j'ai demandé à l'auteur de Nose2 si je devais actuellement utiliser Nose1 ou Nose2 : « In 2013, I need to use nose1 or nose2 ? What is the future of nose ? ».
Pour résumer sa réponse :
  • Nose1 est en mode maintenance
  • L'avenir est du côté de Nose2
  • Jason Pellerin utilise de plus en plus Nose2 à la place de Nose1

Suite à cela, j'ai pris la décision d'utiliser quand c'est possible Nose2 à la place de Nose1.

Différences d'utilisation entre Nose2 et Nose1

En dehors du fait que l'exécutable de Nose1 se nomme nosetests et que celui de Nose2 se nomme simplement nose2, voici quelques différences d'utilisation que j'ai remarquées :

  • L'option -f permet d'arrêter les tests dès que nose rencontre une erreur (cette option était -x avec nose1)

  • L'affichage de la sortie standard est activé par défaut dans nose2. Avec nose1 il faut utiliser l'option -s pour activer l'affichage de la sortie standard.

  • Changement de syntaxe pour le lancement direct d'un ou plusieurs tests :

    Avec Nose1 :

    $ nosetests tests/test1.py:test_foo
    

    La même chose en Nose2 :

    $ nosetests tests.test1.test_foo
    
    À noter qu'avec nose1 le chemin du test à exécuter était relatif au dossier courant.
    Avec nose2 c'est relatif au dossier racine des tests.

Voici plusieurs méthodes pour indiquer la racine des tests :

  • par défaut c'est le dossier courant

  • il est possible d'indiquer le dossier racine via l'option -s, exemple :

    $ nose2 -s tests/
    
  • il est possible de configurer le dossier racine via le fichier de configuration unittest.cfg (fonctionnalité disponible dans le HEAD du projet ou dans la prochaine release), exemple :

    [unittest]
    start-dir=tests
    

En dehors de cela, j'utilise l'option verbose -v mais rien de particulier à ce niveau.

Utilisation de Discover

Depuis Python 2.7, il est possible de lancer les tests avec la fonction discover qui est directement intégré dans unittest (la version 2).

À tort ou à raison, je n'ai pratiquement jamais utilisé cette fonctionnalité discover.

Exemple :

$ python -m unittest discover -s tests
.............
----------------------------------------------------------------------
Ran 13 tests in 9.969s

D'après ce que j'ai pu observer, python -m unittest discover utilise les mêmes noms d'options que nose2.

Il est aussi possible de lancer directement un test, exemples :

$ python -m unittest test_basic
$ python -m unittest test_basic.TestClass
$ python -m unittest test_basic.TestClass.test_method

À noter que la syntaxe ci-dessous ne permet pas de spécifier le dossier racine des tests.

Pour le moment, l'avantage que je trouve à nose2 par rapport à python -m unittest est le support du fichier de configuration unittest.cfg.

Par contre, je n'ai pas testé s'il est possible d'activer le contôle de couverture de code avec python -m unittest.

Où placer ses tests

Une des premières questions que je me suis posées lorsque j'ai commencé à faire des tests unitaires en python, c'est où placer mes tests.

Deux choix :

  1. soit placer les tests à la racine du projet à côté des modules de l'application ou de la librairie, exemple :

    .
    ├── README.txt
    ├── myproject
    │   ├── __init__.py
    │   └── main.py
    ├── setup.py
    ├── tests
    │   └── test_basic.py
    └── unittest.cfg
    
  2. soit placer les tests dans le dossier qui contient les modules de l'application ou de la librairie, exemple :

    .
    ├── README.txt
    ├── myproject
    │   ├── __init__.py
    │   ├── main.py
    │   └── tests
    │       ├── __init__.py
    │       └── test_basic.py
    └── setup.py
    

J'ai personnellement une préférence pour le choix numéro 1. Pourquoi :

  • pour moi les tests ne font pas partie du coeur du code source de ma librairie ou de mon application
  • je trouve que cela n'a pas de sens de faire import myproject.tests.TestCase

Si l'on regarde de nombreux projets python, les deux solutions sont utilisées même si j'ai l'impression qu'il y a une petite domination pour la solution numéro 2.

Pour le moment, je n'ai pas trouvé d'argument absolu en faveur de l'une des deux solutions.

La suite...

Il y a encore deux sujets que je souhaitais aborder dans ce billet :

Ces deux sujets seront sans doute développés dans deux prochains billets.

Read and Post Comments

Mon cookbook : pip, virtualenv

Je présente dans cet article mon workflow concernant l'utilisation de virtualenv et pip

Je tiens à préciser qu'il y a d'autres workflows possibles, par exemple l'utilisation de buildout mais ce n'est pas le sujet de cet article.

Méthode « classique »

Voici la méthode que je nomme "classique" car je pense qu'elle est beaucoup utilisée. J'utilisais cette méthode il y a encore quelques mois.

Prérequis sur la machine (je ne précise pas leur installation, ce n'est pas mon sujet) :

Je prépare un dossier de travail et je clone mon projet :

$ mkdir ~/my_project/
$ cd ~/my_project/
$ git clone http://stephane@repos.stephane-klein.info/example1 .
$ ls -1
devel-requirements.txt
example1
requirements.txt
setup.py
tests
unittest.cfg

Je crée un environnement virtuel python dans le dossier courant :

$ virtualenv .

Comme mon projet est un package python, mon dossier contient un fichier setup.py.

Je lance son installation mais avec l'option -e pour qu'il soit éditable.

$ bin/pip install -e .

Je peux maintenant utiliser mon package :

$ bin/python
>>> import example1
>>> example1.hello_world()
Hello world
Voila, mon installation en mode développement est finie… La seule difficulté pour certaines personnes c'est l'installation des dépendances de bases (distribute/setuptools, pip, virtualenv).
La suite de l'article présente une solution plus simple, avec moins de dépendances.

Installation en mode « édition » ?

Voici deux méthodes pour installer un package python à partir du fichier setup.py :

$ python setup.py install

ou

$ pip install .

Ces deux commandes installent le package dans le dossier site-packages de votre environnement python.

Si vous modifiez le code source de votre package, vos modifications ne seront pas "visibles" lors de l'exécution du package dans votre environnement, car une copie des fichiers sources a été faite lors de l'installation.
Par conséquent, à chaque modification, vous allez devoir installer de nouveau votre package.

Ceci est très pénible si vous êtes en train de développer votre application.

C'est là que l'installation en mode editable entre en jeu.
Lors de l'installation en mode édition, le code source n'est pas copié vers le dossier site-packages mais un lien symbolique est utilisé.
Vos modifications seront tout le temps pris en compte, sans passer par l'étape d'installation.

Voici deux commandes pour effectuer une installation en mode « édition » :

$ python setup.py develop

ou

$ pip install -e .

Par le passé, python setup.py develop ne permettait pas la désinstalation du package. Maintenant ce n'est plus le cas, par conséquent ces deux commandes semblent être synonymes.

Utilisation des fichiers requirements.txt

L'option --requirement de pip est intéressante, elle permet d'installer des dépendances depuis un fichier texte.

Bon, je pense que je n'apprends rien à personne, cette option est très connue.

Par contre, sa syntaxe est moins bien connue, exemples :

  • -e package permet d'installer un package en mode édition
  • -e . est équivalent à pip install -e . ou python setup.py develop
  • -e https://github.com/pypa/pip.git#egg=pip fait un clone du dépôt dans src/pip/ et installe le package en mode édition
  • -r requirements.txt il est possible d'installer un autre fichier de requirements

Il est souvent utile d'avoir des dépendances de packages pour la version de production et des dépendances supplémentaires pour le mode développement.

Par conséquent on trouve souvent deux fichiers requirements :

  • requirements.txt
  • devel-requirements.txt

Exemple de fichier devel-requirements.txt que j'utilise :

-e .
git+https://github.com/nose-devs/nose2.git#egg=nose2
sphinx
Sphinx-PyPI-upload

Explications :

  • j'utilise -e . pour installer le package sur lequel je suis en train de travailler
  • j'installe une version qui n'est pas encore releasé de nose2
  • j'installe des outils de documentation dont j'ai besoin en mode développement

Donc pour installer la version de développement de mon projet, je fais :

$ pip install -r devel-requirements.txt

Installation de requirements.txt depuis setup.py

Il est possible d'avoir un setup.py qui utilise la liste des dépendances de requirements.txt.

Exemple :

from setuptools import setup, find_packages


def parse_requirements(file_name):
    requirements = []
    for line in open(file_name, 'r').read().split('\n'):
        if re.match(r'(\s*#)|(\s*$)', line):
            continue
        if re.match(r'\s*-e\s+', line):
            requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', line))
        elif re.match(r'\s*-f\s+', line):
            pass
        else:
            requirements.append(line)

    return requirements

setup(
    name='my_project',
    version='0.1.0',
    packages=find_packages(),
    include_package_data=True,
    zip_safe=False,
    install_requires=parse_requirements("requirements.txt")
)

Ici, parse_requirements lit le fichier requirements.txt et utilise sont contenu avec l'argument install_requires de la fonction setup.

Mais attention ! La fonction setup ne sait pas traiter la syntax spécifique à pip. Par exemple, les lignes du style git+http... ne fonctionnneront pas.

Méthode avec bootstrap de virtualenv

Depuis quelques mois, j'utilise la fonctionnalité « Creating Your Own Bootstrap Scripts » de virtualenv.

Cette fonctionnalité permet de générer un fichier python, qui installera automatiquement un environnement virtuel comme la commande virtualenv . mais sans aucune dépendance à installer. pip sera bien présent dans bin même si vous ne l'avez pas installé avant.

Utilisation de bootstrap.py

Avant de voir comment générer un fichier bootstrap.py, nous allons voir comment l'utiliser :

$ mkdir ~/my_project/
$ cd ~/my_project/
$ git clone http://stephane@repos.stephane-klein.info/example1.1 .
$ python bootstrap.py
$ bin/pip install -r devel-requirements.txt

Voila, rien de plus… on va voir par la suite qu'il est même possible d'installer automatiquement les devel-requirements.txt.

Génération d'un bootstrap.py

La documentation de virtualenv, donne une exemple de génération d'un fichier bootstrap.py.

Personnellement, je place un fichier nommé create-bootstrap.py dans le dossier où je souhaite créer mon fichier bootstrap.py. Ce fichier create-bootstrap.py contient le code suivant :

import virtualenv, textwrap

output = virtualenv.create_bootstrap_script(textwrap.dedent("""
import os, subprocess

def adjust_options(options, args):
    if len(args) == 0:
        args.append('.')

def after_install(options, home_dir):
    subprocess.call([
        os.path.join('bin', 'pip'),
        'install', '-r', 'devel-requirements.txt'
    ])
"""))
f = open('bootstrap.py', 'w').write(output)

La ligne args.append('.') indique qu'au lancement de bootstrap.py l'environnement python sera installé dans le dossier courant.

Un peu plus bas, je lance l'installation de devel-requirements.txt.

J'ai juste à lancer (une fois) python create-bootstrap.py pour générer bootstrap.py :

$ ls -1
create-bootstrap.py
setup.py
devel-requirements.txt
requirements.txt
$ python create-bootstrap.py
$ ls -1
bootstrap.py
create-bootstrap.py
devel-requirements.txt
requirements.txt
setup.py

Maintenant, j'ajoute create-bootstrap.py et bootstrap.py dans mon dépôt.

$ git add create-bootstrap.py bootstrap.py

Par la suite, il n'y a plus besoin d'utiliser create-bootstrap.py à moins de vouloir modifier bootstrap.py pour ajouter/supprimer des actions automatiques.

Pour finir, mon workflow complet

Prérequis:

  • python

Simple niveau prérequis, non ? Un peu comme buildout pour ceux qui connaissent.

Je prépare un dossier avec mon projet :

$ mkdir ~/my_project/
$ cd ~/my_project/
$ git clone http://my-project.org/ .

En une seule commande, j'installe mon environnement python et mon projet en mode développement (éditable).

$ python bootstrap.py
Read and Post Comments

Comment faciliter la lecture de ses routes sous Pyramid ?

Je souhaite vous faire part de mes réflexions à propos de la déclaration de routes sous Pyramid.

Voici un extrait de la déclaration des routes du projet Bookie (Python based delicious.com replacement).

C'est un extrait du fichier bookie/routes.py :

config.add_route("home", "/")
config.add_route("dashboard", "/dashboard")

# auth routes
config.add_route("login", "login")
config.add_route("logout", "logout")
config.add_route("reset", "{username}/reset/{reset_key}")
config.add_route("signup", "signup")
config.add_route("signup_process", "signup_process")

# celery routes
config.add_route("celery_hourly_stats", "jobhourly")

# DELAPI Routes
config.add_route("del_post_add", "{username}/delapi/posts/add")
config.add_route("del_post_delete", "{username}/delapi/posts/delete")
config.add_route("del_post_get", "{username}/delapi/posts/get")
config.add_route("del_tag_complete", "{username}/delapi/tags/complete")

# bmark routes
config.add_route("bmark_recent", "recent")
config.add_route("bmark_recent_tags", "recent/*tags")

config.add_route("bmark_readable", "bmark/readable/{hash_id}")

# user based bmark routes
config.add_route("user_bmark_recent", "{username}/recent")
config.add_route("user_bmark_recent_tags", "{username}/recent/*tags")

config.add_route("user_bmark_edit", "{username}/edit/{hash_id}")
config.add_route("user_bmark_edit_error",
    "{username}/edit_error/{hash_id}")
config.add_route("user_bmark_new", "{username}/new")
config.add_route("user_bmark_new_error", "{username}/new_error")

# config.add_route("bmark_delete", "/bmark/delete")
# config.add_route("bmark_confirm_delete", "/bmark/confirm/delete/{bid}")

# tag related routes
config.add_route("tag_list", "tags")
config.add_route("tag_bmarks", "tags/*tags")

Voici maintenant la liste des fichiers présents dans le dossier views :

.
├── bcelery
├── __init__.py
├── lib
├── models
├── routes.py
├── routes.pyc
├── scripts
├── static
├── templates
├── tests
└── views               <=== ce dossier
    ├── accounts.py
    ├── api.py
    ├── auth.py
    ├── bmarks.py
    ├── delapi.py
    ├── exceptions.py
    ├── __init__.py
    ├── stats.py
    ├── tags.py
    └── utils.py

Maintenant, je vous pose les questions suivantes :

  • En lisant le code source ci-dessous, pouvez vous me dire où sont les fonctions qui gèrent les vues home et dashboard ?
config.add_route("home", "/")
config.add_route("dashboard", "/dashboard")
  • En lisant le code source ci-dessous, pouvez vous me dire où sont les fonctions qui gèrent les vues user_bmark_recent, user_bmark_recent_tags et user_bmark_edit ?
config.add_route("user_bmark_recent", "{username}/recent")
config.add_route("user_bmark_recent_tags", "{username}/recent/*tags")

config.add_route("user_bmark_edit", "{username}/edit/{hash_id}")

Il n'est pas possible de répondre à ces questions facilement… il faut soit ouvrir les fichiers dans le dossier views ou utiliser grep pour faire une recherche.

Dans ce code source, il n'y a que les commentaires au dessus des routes qui nous permettent d'avoir un indice du fichier où se trouvent les fonctions views.

Les chemins complets vers les fonctions views ne sont pas non plus visibles dans l'outil de debug de Pyramid :

Maintenant, voici une autre manière de déclarer ses routes :

config.add_route("home", "/")
config.add_view(name="home", view="bookie.views.home")

config.add_route("dashboard", "/dashboard")
config.add_view(name="dashboard", view="bookie.views.stats.dashboard")

# auth routes
config.add_route("login", "login")
config.add_view(name="login", view="bookie.views.auth.login")

config.add_route("logout", "logout")
config.add_view(name="logout", view="bookie.views.auth.logout")

config.add_route("reset", "{username}/reset/{reset_key}")
config.add_view(name="reset", view="bookie.views.auth.reset")

config.add_route("signup", "signup")
config.add_view(name="signup", view="bookie.views.auth.signup")

config.add_route("signup_process", "signup_process")
config.add_view(name="signup_process", view="bookie.views.auth.signup_process")

…

Ici, en utilisant une déclaration explicite config.add_view je sais directement où sont présentent les fonctions views.

Je trouve cette notation bien plus pratique pour une personne qui souhaite contribuer, comprendre rapidement la structure d'un projet Pyramid.

Qu'en pensez vous ? Quels sont vos pratiques pour faciliter la lecture de vos projets Pyramid ?

Read and Post Comments

Réflexions à propos de NodeJS et de Javascript plus globalement

Cela fait quelques jours que je me pose la question suivante : « Est-ce que Node.js ne va pas devenir une technologie incontournable / majeur dans les 2 ans qui viennent ? »

Le contexte

Je suis un développeur Python depuis de nombreuses années. J'aime ses librairies, j'aime ses outils, j'aime sa communauté.
J'aime tellement sa syntaxe que quand je vois la syntaxe d'autres langages, j'ai une réaction quelque peu épidermique à la lecture du code.
Avec le temps, l'habitude de la syntaxe Python minimaliste proche d'un pseudo code rend difficile la possibilité d'apprécier un autre langage. Je ne sais pas si c'est positif ou négatif, je me pose simplement la question. Enfin ceci est un autre sujet.

Cependant, mon regard se tourne de plus en plus vers Node.js... je n'ai pas franchi le pas, je n'ai rien développé en Node.js… mais je me demande si je ne passe pas à coté de quelque chose d'important.

Les forces de Node.js

  • Bien que Node.js date seulement de 2009 :

  • tous les développeurs web (front-end) connaissent le langage, il est très populaire. Par conséquent, pour passer à Node.js il n'y a pas le frein d'apprendre un nouveau langage comme ça serait le cas avec l'utilisation d'un framework web basé sur Ruby ou Python. Je pense que dans les mois qui viennent, de nombreux développeurs PHP vont passer à Node.js… surtout ceux qui regardaient ailleurs mais qui ne souhaitent pas apprendre un nouveau langage.

  • la possibilité de partager du code (librairies communes…) entre la partie client et serveur peut être intéressant. La question se pose de plus en plus étant donnée qu'on est de plus en plus amené à effectuer beaucoup de traitement coté client (exemple avec Backbone.js).
    Dernièrement, dans tous mes projets de développement web, j'ai utilisé un moteur de template coté client en plus du moteur de template coté serveur.
    Je dois gérer de plus en plus souvent une couche modèle coté client. Des vues coté client… Cela fait donc plein d'éléments en doubles, utilisés une fois sur le serveur, une fois sur le client. Donc deux technologies à maîtriser.
    Ces derniers jours, la visualisation du screencast du framework javascript Meteor m'a mis la puce à l'oreille.
    Autre exemple, il est possible d'utiliser la même API Canvas coté client et coté serveur (avec node-canvas).
  • Node.js semble être très rapide, ça tient très bien la monté en charge.

  • La vitesse de l'interpréteur Javascript est en constante progression V8 (JavaScript engine)

Faiblesses de Node.js

Je ne sais pas si mes remarques sont exactes, je n'ai aucune expérience en Node.js.

  • Exemple dans la partie Bad Use Cases du guide Felix's Node.js Convincing the boss guide il est indiqué que les framework Node.js sont moins matures que ce que l'on peut trouver en Ruby, Python et Php (bon pour ce dernier j'ai des doutes, je pense que la communauté Node.js est déjà plus mature, plus structurée que la communauté Php… enfin bon…).
  • Je ne pense pas que Sequelize soit aussi mature que SQLAlchemy
  • Je n'ai pas trouvé d'outil comme Whoosh en Javascript (c'est un moteur de recherche léger). J'ai tout de même trouvé Node-xapian

Je ne sais pas si il est encore tôt pour passer à Node.js mais si la communauté continue à être aussi dynamique que ces 2 dernières années… alors les pièces manquantes seront bientôt créées.

Est-ce que l'on va retrouver Javascript partout ?

Voici une petite liste :

Quand je vois cette liste, je me pose sérieusement la question : est-ce que Javascript est le langage incontournable de ces 10 prochaines années ? Certes il est déjà incontournable dans le navigateur, mais pour le reste ?


Édition 5 mai 2012 :

  • j'ai publié ce billet sur LinuxFR, j'ai reçu 90 commentaires…
  • j'ai publié ce billet sur la mailing list de l'Afpy, j'ai reçu 25 commentaires… malheureusement les archives de cette liste n'est pas publique

merci beaucoup pour vos retours d'expériences !

Read and Post Comments

Screencast de présentation de Tacot

J'ai créé un screencast de présentation de Tacot.

J'ai plus ou moins suivi le contenu de la documentation… Dans la vidéo j'ai fait quelques erreurs d'expression alors soyez indulgents… ce n'est pas facile de faire un screencast valide du premier coup.

Je vous conseille de visualiser la vidéo en mode 720p afin de bien pouvoir lire le contenu de la console. Après la réalisation du screencast je me suis rendu compte que j'aurais dû agrandir la taille des polices.

Lien vers la documentation de Tacot.

Read and Post Comments
Billets plus anciens »

Mes flux :