Fichiers statiques et le cache de Apache

Contexte

Depuis des années, je suis embêté par gestion du cache du navigateur sur les fichiers statiques servis par le serveur HTTP Apache.

Encore une fois, ce matin, ce problème de cache me pose problème : alors que j'avais publié une mise à jour sur la page d'actualité du site Coworking Metz, je constate qu'un certain nombre de personnes ne voient pas la mise à jour, car pour la voir il faut vider le cache du navigateur en faisant par exemple un CTRL-F5 sur la page.

J'ai donc décidé d'en savoir plus… pourquoi la configuration par défaut de Apache gère "mal" à mes yeux le cache.

Le comportement que je souhaite avoir est celui-ci :

« Je souhaite que dès que je modifie le contenu d'un fichier statique, celui-ci soit tout de suite visible par mes visiteurs »

Analyse

Lors de mes développements web, j'ai déjà "joué" avec les champs header Cache-Control, If-Modified-Since, ETag, Expires, Last-Modified

Je me suis dit « Apache doit envoyer un champ Expires avec une valeur par défaut ».

J'ai cherché dans la documentation de Apache et je n'ai rien trouvé au sujet de Expires en dehors du module Expires qui n'était pas activé sur mon serveur.

J'ai ensuite cherché à analyser les champs HTTP Response retournés par le serveur sur le site Coworking Metz.

Dans un premier temps, j'ai regardé du coté de Firebug mais je n'ai pas trouvé comment voir le résultat de la requête HTTP de la page courante (dans Network on voit uniquement les ressources de la page).

J'ai ensuite trouvé l'extension HttpFox (que l'on active via le menu Affichage => HttpFox).

Avec cette extension, j'ai pu voir que le serveur Apache retourne les champs Header suivants :

(Status-Line)        HTTP/1.1 304 Not Modified
Date                 Sat, 04 Jan 2014 11:19:29 GMT
Server               Apache/2.2.22 (Debian)
Connection           Keep-Alive
Keep-Alive           timeout=5, max=100
Etag                 "1920d20-442b-4ef22c61a1396"
Vary                 Accept-Encoding

Contrairement à ce que je pensais, par défaut Apache ne retourne pas le champ Expires.

C'est donc Firefox qui a une valeur par défaut et cette valeur par défaut est expliqué ici : How are expiration times calculated (since not every response includes an Expires header) ?.

Le texte suivant mérite un exemple pour être plus facilement compris :

Finally, if neither header is present, then we look for a "Last-Modified" header. If this header is present, then the cache's freshness lifetime is equal to the value of the "Date" header minus the value of the "Last-modified" header divided by 10. This is the simplified heuristic algorithm suggested in RFC 2616 section 13.2.4.

Exemple :

  • la dernière fois que le fichier index.html a été modifié : 2014-01-04 à 11h00 (last_modified_time)
  • le 2014-01-04 à 12h00 le navigateur fait une première requête (date_value)
age_expiration = (date_value - last_modified_time) * 0.1 => 3600 secondes * 0.1 => 360s
  • le 2014-01-04 à 12h01 le navagiteur fait une seconde requête, utilisation du cache car 60s < 360s
  • le 2014-01-04 à 12h10 le navigateur fait une troisième requête, le cache n'est pas utilisé car : 600s > 360s

Conclusion pour atteindre mon objectif, mes requêtes HTTP doivent retourner Expires: 0.

Solution : configuration de Apache

Activation du module Expires :

# a2enmod expires
# service apache2 restart

Placer un fichier .htaccess à la racine du site, avec le contenu suivant :

<IfModule mod_expires.c>
ExpiresActive on
ExpiresDefault "access plus 0 seconds"
</IfModule>

Code HTTP 304

Avec cette configuration, l'activité sur le serveur est certe plus élevée… mais un cache est toujours présent.

Lorsque le navigateur envoie la requête HTTP avec les paramètres suivants :

(Request-Line)      GET /news.html HTTP/1.1
Host                coworking.a-metz.info
Accept-Language     fr,en;q=0.8,fr-fr;q=0.5,en-us;q=0.3
Accept-Encoding     gzip, deflate
Connection          keep-alive
If-Modified-Since   Sat, 04 Jan 2014 11:33:53 GMT
If-None-Match       "1920d20-442d-4ef2366c0e11a"

en fonction de la date de modification du fichier news.html et de la valeur du champ If-Modified-Since, le serveur va renvoyer une réponse avec un Status 200 ou 304.

Dans le cas d'une réponse 304, le contenu de la page n'est pas envoyé au navigateur, c'est le cache du navigateur qui est utilisé.

Conclusion, avec des réponses 304, l'activité du serveur est certe supérieur mais consommation de la bande passante est tout de même limitée car le contenu des pages, des images… n'est pas transmis.

Qu'en est-t-il de Nginx ?

Par défaut, Nginx ne renvoie pas de champ HTTP ETag, ni de champ Expires.

J'ai observé que quand le serveur HTTP ne retourne pas de champ ETag alors le navigateur va interroger le serveur avec un champ HTTP If-Modified-Since pour savoir si il faut ou non utiliser le cache du navigateur.

Conclusion, la configuration par défaut de Nginx correspond au comportement que je souhaite, celui indiqué en début de billet.

Read and Post Comments

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

Ember.js - Question « Un adaptateur modèle <=> template ? »

Problématique

Depuis quelques mois, j'utilise Ember.js dans certain de mes projets.

Dernièrement, j'ai été confronté au problème présenté ci-dessous.

Par exemple, j'ai cette structure de données (couche modèle) :

* Lundi
    * Évenement 1
    * Évenement 2
    * Evenement 3

* Mardi
    * Évenement 4
    * Évenement 5

* Mercredi
    * Évenement 6
    * Évenement 7
    * Évenement 8

Avec deux boucles, je peux facilement réaliser le tableau suivant :

Lundi Évenement 1 Évenement 2 Évenement 3
Mardi Évenement 4 Évenement 5  
Mercredi Évenement 6 Évenement 7 Évenement 8

Pour faire ce tableau, je peux utiliser un template du style :

<table>
    {{#each day in App.day_list}}
    <tr>
        <td>
            {{ day.name }}
        </td>
        {{#each event in day.events}}
        <td>
            {{ event.name }}
        </td>
        {{/each}}
    </tr>
    {{/each}}
</table>

Par contre, il est bien plus difficile (sans changer la structure de données) de réaliser le tableau suivant :

Lundi Mardi Mercredi
Évenement 1 Évenement 4 Évenement 6
Évenement 2 Évenement 5 Évenement 7
Évenement 3   Évenement 8

Ici le tableau est inversé… je ne peux donc pas réaliser ce tableau avec les deux mêmes boucles utilisées dans l'exemple précédent.

La seul solution que j'ai trouvé c'est de fournir un objet de type "vue" qui adapte mon modèle au besoin de ma vue. Seulement ce n'est pas si simple… car :

  • il faut que si les données sont modifiées, que le template soit automatiquement mis à jour… il faut donc une gestion d'évenement entre les objets de la couche modèles et l'objet "vue"
  • en cas d'interactions au niveau de l'interface utilisateur il faut que les données soient automatiquement mis à jour.

Premier exemple, simple

Il est possible de tester le premier exemple simple via ce lien jsFiddle.

Ce qu'il est possible de voir et tester :

  • cliquez sur les liens « Step 1 », « Step 2 », « Step 3 », « Step 4 »
  • ces liens modifient le contenu de la couche modèle
  • dès la première étape, vous pouvez voir deux tableaux identiques… qui affichent le contenu de la couche modèle
  • à chaque étape, vous pouvez modifier la valeur des cellules des tableaux
  • lorsque la valeur d'une cellule est modifiée dans un des deux tableaux, la cellule dans le second tableau est aussi modifiée

Voici ci-dessous un petit screencast de ce premier exemple simple :

Recherche d'une solution pour le « Tableau pivoté »

Comme indiqué plus haut, je cherche une solution pour afficher le même tableau mais pivoté (voir exemple au dessus).

J'ai un début de solution dans ce jsFiddle.

Vous pouvez constater que les étapes 1, 2, 3 fonctionnent.
L'étape 4 ne fonctionne pas…

Au niveau du code source, vous pouvez voir cette ligne commentée à plusieurs endroits :

// App.planning.rows.sync();

C'est cette fonction sync qui synchronise mon objet planning avec les données de la couche modèle dont l'objet de racine est day_list. Cette méthode est automatiquement exécutée par un observateur quand il y a des modifications au niveau de l'objet App.day_list (les sync en commentaire sont simplement là pour voir le comportement que devrait avoir le programme si cette méthode était bien exécutée) :

Ember.addObserver(
    App.day_list,
    '@each.content.@each',
    App.planning.rows,
    'sync'
);

C'est cet observateur qui fonctionne bien pour l'étape 1, 2 et 3 mais pas pour l'étape 4.

Mes questions

  • Avez vous une solution pour que sync soit bien exécuté lors de l'étape 4 ?
  • Est-ce que mon approche est la bonne ?
  • Quel est le nom standard de l'objet que j'appelle "vue" ? est-ce que je suis dans le vrai en disant que c'est un objet de type "vue" ?
  • Est-ce qu'il existe un pattern classique pour ce type de besoin en Ember.js ?
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

Billets plus anciens »

Consultez mes archives pour avoir une vue d'ensemble de mes anciens billets.

Mes flux :